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中的呢?
为了解答上面的两个问题,本文将进一步分析android build system源码。
android build system是google提供的一组用来构建、运行、测试和打包我们app的工具集,包含了aapt
、aidl
、javac
、dex
、apkbuilder
、Jarsigner
、zipalign
等工具。在我们构建app时,build进程会去按一定顺序调用上述工具来生成相应文件,而最终的输出将会是一个完整的可安装的.apk文件,构建流程如下:
构建系统先从product flavors, build types和dependencies中合并资源,如果不同目录下有重名资源,将按以下优先级进行覆盖:
dependencies > build types > product flavors > main source directory
|
本文重点对第4步
中.class经过dex到.dex过程源码进行分析。
为了更好地分析.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执行过程中都做了什么。
android build system中dex过程发生在DexTask,DexTask关联的Transform是DexTransform。
当DexTransform.transfrom方法被调用时,会先创建并初始化main目录作为输出dex的目录,然后调用androidBuilder.convertByteCode方法进行.class到.dex的转换,此时jarInputs为classes.jar,directoryInputs长度为空,传递的boolean类型的multiDex参数来自build.gralde文件中在defaultConfig
对multiDexEnabled = 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(); } |
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分为三类:
如果输入是.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方法主要做了以下几件事:
由此可以看出:
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); } } } |
通过对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的遍历顺序添加的。
文章浏览阅读1w次,点赞2次,收藏27次。来源:机器人小妹 很多时候企业拥有重复,乏味且困难的工作流程,这些流程往往会减慢生产速度并增加运营成本。为了降低生产成本,企业别无选择,只能自动化某些功能以降低生产成本。 通过数字化..._人工智能平台
文章浏览阅读2.2k次。热加载能够在每次保存修改的代码后自动刷新 electron 应用界面,而不必每次去手动操作重新运行,这极大的提升了开发效率。安装 electron 热加载插件热加载虽然很方便,但是不是每个 electron 项目必须的,所以想要舒服的开发 electron 就只能给 electron 项目单独的安装热加载插件[electron-reloader]:// 在项目的根目录下安装 electron-reloader,国内建议使用 cnpm 代替 npmnpm install electron-relo._electron-reloader
文章浏览阅读942次。在11.0 进行定制化开发,会根据需要去掉recovery模式的一些选项 就是在device.cpp去掉一些选项就可以了。_android recovery 删除 部分菜单
文章浏览阅读3.7k次。https://www.yuque.com/mnn/cn/cvrt_linux_mac基础依赖这些依赖是无关编译选项的基础编译依赖• cmake(3.10 以上)• protobuf (3.0 以上)• 指protobuf库以及protobuf编译器。版本号使用 protoc --version 打印出来。• 在某些Linux发行版上这两个包是分开发布的,需要手动安装• Ubuntu需要分别安装 libprotobuf-dev 以及 protobuf-compiler 两个包•..._mnn 编译linux
文章浏览阅读1.8k次。CSS3新增动画属性“@-webkit-keyframes”,从字面就可以看出其含义——关键帧,这与Flash中的含义一致。利用CSS3制作动画效果其原理与Flash一样,我们需要定义关键帧处的状态效果,由CSS3来驱动产生动画效果。下面讲解一下如何利用CSS3制作淡入淡出的动画效果。具体实例可参考刚进入本站时的淡入效果。1. 定义动画,名称为fadeIn@-webkit-keyf_css3入场效果淡入淡出
文章浏览阅读2.8k次。计算机系统应包括硬件和软件两个子系统,硬件和软件又必须依次分别包括中央处理器和系统软件。按人的要求接收和存储信息,自动进行数据处理和计算,并输出结果信息的机器系统。计算机是脑力的延伸和扩充,是近代科学的重大成就之一。计算机系统由硬件(子)系统和软件(子)系统组成。前者是借助电、磁、光、机械等原理构成的各种物理部件的有机组合,是系统赖以工作的实体。后者是各种程序和文件,用于指挥全系统按指定的要求进行..._计算机系统包括硬件系统和软件系统 软件又必须包括
文章浏览阅读7.9k次,点赞3次,收藏22次。一 定义这是最早出现的置换算法。该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰。该算法实现简单,只需把一个进程已调入内存的页面,按先后次序链接成一个队列,并设置一个指针,称为替换指针,使它总是指向最老的页面。但该算法与进程实际运行的规律不相适应,因为在进程中,有些页面经常被访问,比如,含有全局变量、常用函数、例程等的页面,FIFO 算法并不能保证这些页面不被淘汰。这里,我_进程调度fifo算法代码
文章浏览阅读133次。rownum是oracle才有的写法,rownum在oracle中可以用于取第一条数据,或者批量写数据时限定批量写的数量等mysql取第一条数据写法SELECT * FROM t order by id LIMIT 1;oracle取第一条数据写法SELECT * FROM t where rownum =1 order by id;ok,上面是mysql和oracle取第一条数据的写法对比,不过..._mysql 替换@rownum的写法
文章浏览阅读790次,点赞3次,收藏4次。官网下载下载链接:http://www.eclipse.org/downloads/点击Download下载完成后双击运行我选择第2个,看自己需要(我选择企业级应用,如果只是单纯学习java选第一个就行)进入下一步后选择jre和安装路径修改jvm/jre的时候也可以选择本地的(点后面的文件夹进去),但是我们没有11版本的,所以还是用他的吧选择接受安装中安装过程中如果有其他界面弹出就点accept就行..._ecjelm
文章浏览阅读245次。原文链接:https://linux.cn/article-7801-1.htmlifconfigping <IP地址>:发送ICMP echo消息到某个主机traceroute <IP地址>:用于跟踪IP包的路由路由:netstat -r: 打印路由表route add :添加静态路由路径routed:控制动态路由的BSD守护程序。运行RIP路由协议gat..._ifconfig 删除vlan
文章浏览阅读224次。reduxredux里要求把数据都放在公共的存储区域叫store里面,组件中尽量少放数据,假如绿色的组件要给很多灰色的组件传值,绿色的组件只需要改变store里面对应的数据就行了,接着灰色的组件会自动感知到store里的数据发生了改变,store只要有变化,灰色的组件就会自动从store里重新取数据,这样绿色组件的数据就很方便的传到其它灰色组件里了。redux就是把公用的数据放在公共的区域去存..._redux redis
文章浏览阅读2.2k次,点赞3次,收藏6次。unzip版本不支持4G以上的压缩包所以要使用p7zip:Linux一个高压缩率软件wget http://sourceforge.net/projects/p7zip/files/p7zip/9.20.1/p7zip_9.20.1_src_all.tar.bz2tar jxvf p7zip_9.20.1_src_all.tar.bz2cd p7zip_9.20.1make && make install 如果安装失败,看一下报错是不是因为没有下载gcc 和 gcc ++(p7_linux 7za解压中文乱码