合并Transform , 支持增量编译
This commit is contained in:
@@ -9,6 +9,7 @@ import com.plugin.component.extension.module.ProjectInfo
|
||||
import com.plugin.component.extension.option.debug.DebugDependenciesOption
|
||||
import com.plugin.component.extension.option.publication.PublicationDependenciesOption
|
||||
import com.plugin.component.extension.option.publication.PublicationOption
|
||||
import com.plugin.component.transform.ComponentTransform
|
||||
import com.plugin.component.transform.InjectCodeTransform
|
||||
|
||||
import com.plugin.component.transform.ScanCodeTransform
|
||||
@@ -236,10 +237,10 @@ class ComponentPlugin implements Plugin<Project> {
|
||||
if (it instanceof AppPlugin) {
|
||||
if (projectInfo.isDebugModule() || projectInfo.isMainModule()) {
|
||||
Logger.buildOutput("plugin is AppPlugin")
|
||||
Logger.buildOutput("registerTransform", "ScanCodeTransform")
|
||||
Logger.buildOutput("registerTransform", "InjectCodeTransform")
|
||||
childProject.extensions.findByType(BaseExtension.class).registerTransform(new ScanCodeTransform(childProject))
|
||||
childProject.extensions.findByType(BaseExtension.class).registerTransform(new InjectCodeTransform(childProject))
|
||||
Logger.buildOutput("registerTransform", "ComponentTransform")
|
||||
// Logger.buildOutput("registerTransform", "InjectCodeTransform")
|
||||
// childProject.extensions.findByType(BaseExtension.class).registerTransform(new InjectCodeTransform(childProject))
|
||||
childProject.extensions.findByType(BaseExtension.class).registerTransform(new ComponentTransform(childProject))
|
||||
}
|
||||
}
|
||||
Logger.buildOutput("=====> project[" + childProject.name + "]注入插件 <=====")
|
||||
|
||||
@@ -24,6 +24,7 @@ class Constants {
|
||||
public static String PUBLISHING = 'publishing'
|
||||
public static String LOCAL_PROPERTIES = "local.properties"
|
||||
public static String CLEAN = 'clean'
|
||||
public static String PLUGIN_CACHE = "scan_cache"
|
||||
|
||||
//file
|
||||
public static String DEFAULT_MAIN_MODULE_NAME = "app"
|
||||
|
||||
@@ -33,14 +33,12 @@ public class CodeInjectProcessor {
|
||||
private static final String FILE_SEP = File.separator;
|
||||
|
||||
|
||||
public void injectCode(String filePath) {
|
||||
public void injectCode(String inputPath, String outputPath) {
|
||||
try {
|
||||
if (filePath != null && filePath.endsWith(".jar")) {
|
||||
File file = new File(filePath);
|
||||
weaveJar(file);
|
||||
} else if (filePath != null && filePath.endsWith(".class")) {
|
||||
File file = new File(filePath);
|
||||
weaveSingleClassToFile(file);
|
||||
if (inputPath != null && inputPath.endsWith(".jar")) {
|
||||
weaveJar(inputPath, outputPath);
|
||||
} else if (inputPath != null && inputPath.endsWith(".class")) {
|
||||
weaveSingleClassToFile(inputPath, outputPath);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -55,8 +53,13 @@ public class CodeInjectProcessor {
|
||||
}
|
||||
|
||||
|
||||
public final void weaveSingleClassToFile(File inputFile) throws IOException {
|
||||
File outputFile = new File(inputFile.getParent(), inputFile.getName() + ".temp");
|
||||
public final void weaveSingleClassToFile(String inputPath, String outputPath) throws IOException {
|
||||
File inputFile = new File(inputPath);
|
||||
File outputFile = new File(outputPath);
|
||||
if (outputFile.exists()) {
|
||||
outputFile.delete();
|
||||
}
|
||||
|
||||
FileUtils.touch(outputFile);
|
||||
InputStream inputStream = new FileInputStream(inputFile);
|
||||
byte[] bytes = doGenerateCode(inputStream);
|
||||
@@ -65,27 +68,22 @@ public class CodeInjectProcessor {
|
||||
fos.write(bytes);
|
||||
fos.close();
|
||||
inputStream.close();
|
||||
if (inputFile.exists()) {
|
||||
inputFile.delete();
|
||||
}
|
||||
outputFile.renameTo(inputFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public final void weaveJar(File inputJar) throws IOException {
|
||||
public final void weaveJar(String inputPath, String outputPath) throws IOException {
|
||||
|
||||
File outputJar = new File(inputJar.getParent(), inputJar.getName() + ".temp");
|
||||
File outputJar = new File(outputPath);
|
||||
if (outputJar.exists()) {
|
||||
outputJar.delete();
|
||||
}
|
||||
|
||||
ZipFile inputZip = new ZipFile(inputJar);
|
||||
ZipFile inputZip = new ZipFile(new File(inputPath));
|
||||
ZipOutputStream outputZip = new ZipOutputStream(new BufferedOutputStream(java.nio.file.Files.newOutputStream(outputJar.toPath())));
|
||||
Enumeration<? extends ZipEntry> inEntries = inputZip.entries();
|
||||
|
||||
|
||||
String outputPath = outputJar.getAbsolutePath();
|
||||
while (inEntries.hasMoreElements()) {
|
||||
ZipEntry entry = inEntries.nextElement();
|
||||
InputStream originalFile = new BufferedInputStream(inputZip.getInputStream(entry));
|
||||
@@ -115,17 +113,7 @@ public class CodeInjectProcessor {
|
||||
}
|
||||
outputZip.flush();
|
||||
outputZip.close();
|
||||
|
||||
inputZip.close();
|
||||
|
||||
if (inputJar.exists()) {
|
||||
boolean result = inputJar.delete();
|
||||
System.out.println("delete jar result = " + result);
|
||||
}
|
||||
boolean rename = outputJar.renameTo(inputJar);
|
||||
System.out.println("rename jar result = " + rename);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,13 +13,18 @@ import com.android.build.api.transform.TransformOutputProvider;
|
||||
import com.android.build.gradle.internal.pipeline.TransformManager;
|
||||
import com.android.ide.common.internal.WaitableExecutor;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.gson.Gson;
|
||||
import com.plugin.component.Constants;
|
||||
import com.plugin.component.transform.info.ScanRuntime;
|
||||
import com.plugin.component.transform.info.ScanSummaryInfo;
|
||||
import com.plugin.component.utils.CacheDiskUtils;
|
||||
import com.quinn.hunter.transform.RunVariant;
|
||||
import com.quinn.hunter.transform.asm.ClassLoaderHelper;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.logging.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -105,6 +110,9 @@ public class ComponentTransform extends Transform {
|
||||
for (TransformInput input : inputs) {
|
||||
for (JarInput jarInput : input.getJarInputs()) {
|
||||
Status status = jarInput.getStatus();
|
||||
|
||||
String filePath = jarInput.getFile().getAbsolutePath();
|
||||
|
||||
File dest = outputProvider.getContentLocation(jarInput.getFile().getAbsolutePath(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
|
||||
if (isIncremental && !emptyRun) {
|
||||
switch (status) {
|
||||
@@ -118,6 +126,7 @@ public class ComponentTransform extends Transform {
|
||||
if (dest.exists()) {
|
||||
FileUtils.forceDelete(dest);
|
||||
}
|
||||
ScanRuntime.removedFile(filePath);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -150,6 +159,7 @@ public class ComponentTransform extends Transform {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
destFile.delete();
|
||||
}
|
||||
ScanRuntime.removedFile(inputFile.getAbsolutePath());
|
||||
break;
|
||||
case ADDED:
|
||||
case CHANGED:
|
||||
@@ -172,25 +182,66 @@ public class ComponentTransform extends Transform {
|
||||
}
|
||||
waitableExecutor.waitForTasksWithQuickFail(true);
|
||||
|
||||
ScanSummaryInfo cacheSummary = readPluginCache(); // 读取缓存
|
||||
|
||||
ScanRuntime.logScanInfo();
|
||||
ScanSummaryInfo scanSummaryInfo = ScanRuntime.updateSummaryInfo(cacheSummary); // 整理本次扫码结果,返回最新的模块结构
|
||||
ScanRuntime.buildComponentSdkInfo();
|
||||
ScanRuntime.logScanInfo();
|
||||
|
||||
updatePluginCache(cacheSummary); // 保存缓存
|
||||
|
||||
long costTime = System.currentTimeMillis() - startTime;
|
||||
logger.warn((getName() + "scan code costed " + costTime + "ms"));
|
||||
|
||||
startTime = System.currentTimeMillis();
|
||||
injectCode(scanSummaryInfo);
|
||||
}
|
||||
|
||||
String injectFile = bytecodeWeaver.getInjectClassFile();
|
||||
if (injectFile != null) {
|
||||
logger.warn(" inject file find : file = " + injectFile);
|
||||
CodeInjectProcessor codeInjectProcessor = new CodeInjectProcessor();
|
||||
codeInjectProcessor.injectCode(injectFile);
|
||||
private void injectCode(@NotNull ScanSummaryInfo scanSummaryInfo) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
String injectInputPath = scanSummaryInfo.inputFilePath; // 输入文件
|
||||
String injectOutputPath = scanSummaryInfo.outputFilePath; // 输出文件
|
||||
if (injectInputPath != null) {
|
||||
logger.warn(" inject file find : file = " + injectInputPath);
|
||||
try {
|
||||
CodeInjectProcessor codeInjectProcessor = new CodeInjectProcessor();
|
||||
codeInjectProcessor.injectCode(injectInputPath, injectOutputPath);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.warn("inject Code error");
|
||||
}
|
||||
logger.warn("inject Code end");
|
||||
}
|
||||
|
||||
long injectCostTime = System.currentTimeMillis() - startTime;
|
||||
logger.warn((getName() + " inject code costed " + injectCostTime + "ms"));
|
||||
ScanRuntime.clearScanInfo();
|
||||
ScanRuntime.clearSummaryInfo();
|
||||
}
|
||||
|
||||
private void updatePluginCache(ScanSummaryInfo cacheSummary) {
|
||||
try {
|
||||
String json = new Gson().toJson(cacheSummary);
|
||||
CacheDiskUtils.getInstance(project.getBuildDir()).put(Constants.PLUGIN_CACHE, json);
|
||||
logger.warn((getName() + "save cache success"));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ScanSummaryInfo readPluginCache() {
|
||||
ScanSummaryInfo cacheSummary = null;
|
||||
try {
|
||||
String json = CacheDiskUtils.getInstance(project.getBuildDir()).getString(Constants.PLUGIN_CACHE);
|
||||
cacheSummary = new Gson().fromJson(json, ScanSummaryInfo.class);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (cacheSummary == null) {
|
||||
cacheSummary = new ScanSummaryInfo();
|
||||
}
|
||||
return cacheSummary;
|
||||
}
|
||||
|
||||
private void transformSingleFile(final File inputFile, final File outputFile, final String srcBaseDir) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.plugin.component.transform;
|
||||
|
||||
import com.plugin.component.transform.info.ScanRuntime;
|
||||
import com.quinn.hunter.transform.asm.ExtendClassWriter;
|
||||
import com.quinn.hunter.transform.asm.IWeaver;
|
||||
|
||||
@@ -47,11 +48,11 @@ public class ComponentWeaver implements IWeaver {
|
||||
}
|
||||
|
||||
public final void weaveJar(File inputJar, File outputJar) throws IOException {
|
||||
String filePath = inputJar.getAbsolutePath();
|
||||
String inputPath = inputJar.getAbsolutePath();
|
||||
String outputPath = outputJar.getAbsolutePath();
|
||||
ZipFile inputZip = new ZipFile(inputJar);
|
||||
ZipOutputStream outputZip = new ZipOutputStream(new BufferedOutputStream(java.nio.file.Files.newOutputStream(outputJar.toPath())));
|
||||
Enumeration<? extends ZipEntry> inEntries = inputZip.entries();
|
||||
String outputPath = outputJar.getAbsolutePath();
|
||||
while (inEntries.hasMoreElements()) {
|
||||
ZipEntry entry = inEntries.nextElement();
|
||||
InputStream originalFile = new BufferedInputStream(inputZip.getInputStream(entry));
|
||||
@@ -60,12 +61,12 @@ public class ComponentWeaver implements IWeaver {
|
||||
// seperator of entry name is always '/', even in windows
|
||||
String className = outEntry.getName().replace("/", ".");
|
||||
|
||||
beforeWeaveClass(outputPath, className);
|
||||
beforeWeaveClass(inputPath, outputPath, className);
|
||||
|
||||
if (!isWeavableClass(className)) {
|
||||
newEntryContent = org.apache.commons.io.IOUtils.toByteArray(originalFile);
|
||||
} else {
|
||||
newEntryContent = weaveSingleClassToByteArray(filePath, originalFile);
|
||||
newEntryContent = weaveSingleClassToByteArray(inputPath, originalFile);
|
||||
}
|
||||
CRC32 crc32 = new CRC32();
|
||||
crc32.update(newEntryContent);
|
||||
@@ -88,13 +89,15 @@ public class ComponentWeaver implements IWeaver {
|
||||
if (!inputBaseDir.endsWith(FILE_SEP)) inputBaseDir = inputBaseDir + FILE_SEP;
|
||||
String className = inputFile.getAbsolutePath().replace(inputBaseDir, "").replace(FILE_SEP, ".");
|
||||
|
||||
String filePath = inputFile.getAbsolutePath();
|
||||
beforeWeaveClass(outputFile.getAbsolutePath(), className);
|
||||
String inputPath = inputFile.getAbsolutePath();
|
||||
String outputPath = outputFile.getAbsolutePath();
|
||||
|
||||
beforeWeaveClass(inputPath, outputPath, className);
|
||||
|
||||
if (isWeavableClass(className)) {
|
||||
FileUtils.touch(outputFile);
|
||||
InputStream inputStream = new FileInputStream(inputFile);
|
||||
byte[] bytes = weaveSingleClassToByteArray(filePath, inputStream);
|
||||
byte[] bytes = weaveSingleClassToByteArray(inputPath, inputStream);
|
||||
FileOutputStream fos = new FileOutputStream(outputFile);
|
||||
fos.write(bytes);
|
||||
fos.close();
|
||||
@@ -133,10 +136,13 @@ public class ComponentWeaver implements IWeaver {
|
||||
private static final String sComponentManagerPath = "com.plugin.component.ComponentManager";
|
||||
|
||||
|
||||
public void beforeWeaveClass(String outputFile, String className) {
|
||||
public void beforeWeaveClass(String inputPath, String outputFile, String className) {
|
||||
if (injectClassFile == null && className != null && className.contains(sComponentManagerPath)) {
|
||||
System.out.println("find class ComponentManager : file is : " + outputFile);
|
||||
injectClassFile = outputFile;
|
||||
ScanRuntime.getsSummaryInfo().inputFilePath = inputPath;
|
||||
ScanRuntime.getsSummaryInfo().outputFilePath = outputFile;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,11 +73,13 @@ class ScanCodeAdapter extends ClassVisitor {
|
||||
@Override
|
||||
void visitEnd() {
|
||||
if (scanComponentInfo != null) {
|
||||
ScanRuntime.addComponentInfo(scanComponentInfo)
|
||||
// ScanRuntime.addComponentInfo(scanComponentInfo)
|
||||
ScanRuntime.addComponentInfo(filePath, scanComponentInfo)
|
||||
scanComponentInfo = null
|
||||
}
|
||||
if (scanSdkInfo != null) {
|
||||
ScanRuntime.addSdkInfo(scanSdkInfo)
|
||||
// ScanRuntime.addSdkInfo(scanSdkInfo)
|
||||
ScanRuntime.addSdkInfo(filePath, scanSdkInfo)
|
||||
scanSdkInfo = null
|
||||
}
|
||||
super.visitEnd()
|
||||
|
||||
@@ -10,6 +10,9 @@ class ScanRuntime {
|
||||
static List<ScanSdkInfo> sSdkInfo = new ArrayList<>()
|
||||
static List<ComponentSdkInfo> componentSdkInfoList = new ArrayList<>()
|
||||
|
||||
static ScanSummaryInfo sSummaryInfo = new ScanSummaryInfo()
|
||||
|
||||
|
||||
static void addComponentInfo(@NonNull ScanComponentInfo scanComponentInfo) {
|
||||
sComponentInfo.add(scanComponentInfo)
|
||||
}
|
||||
@@ -19,10 +22,12 @@ class ScanRuntime {
|
||||
}
|
||||
|
||||
static void logScanInfo() {
|
||||
Logger.buildOutput("Dodge sdk info size = " + sSdkInfo.size())
|
||||
for (ScanSdkInfo sdkInfo : sSdkInfo) {
|
||||
Logger.buildOutput(sdkInfo.toString())
|
||||
}
|
||||
|
||||
Logger.buildOutput("Dodge Component Info size = " + sComponentInfo.size())
|
||||
for (ScanComponentInfo scanComponentInfo : sComponentInfo) {
|
||||
Logger.buildOutput(scanComponentInfo.toString())
|
||||
}
|
||||
@@ -68,4 +73,74 @@ class ScanRuntime {
|
||||
static List<ComponentSdkInfo> getComponentSdkInfoList() {
|
||||
return componentSdkInfoList
|
||||
}
|
||||
|
||||
|
||||
static void addSdkInfo(String filePath, @NonNull ScanSdkInfo scanSdkInfo) {
|
||||
Set<ScanSdkInfo> set = sSummaryInfo.updateSdkMap.get(filePath)
|
||||
if (set == null) {
|
||||
set = new HashSet<>()
|
||||
sSummaryInfo.updateSdkMap.put(filePath, set)
|
||||
}
|
||||
set.add(scanSdkInfo)
|
||||
}
|
||||
|
||||
static void addComponentInfo(String filePath, @NonNull ScanComponentInfo componentInfo) {
|
||||
Set<ScanComponentInfo> set = sSummaryInfo.updateComponentMap.get(filePath)
|
||||
if (set == null) {
|
||||
set = new HashSet<>()
|
||||
sSummaryInfo.updateComponentMap.put(filePath, set)
|
||||
}
|
||||
set.add(componentInfo)
|
||||
}
|
||||
|
||||
|
||||
static void removedFile(String path) {
|
||||
sSummaryInfo.removedFileSet.add(path)
|
||||
}
|
||||
|
||||
static ScanSummaryInfo updateSummaryInfo(ScanSummaryInfo cacheSummary) {
|
||||
if (sSummaryInfo.inputFilePath != null) {
|
||||
cacheSummary.inputFilePath = sSummaryInfo.inputFilePath
|
||||
}
|
||||
|
||||
if (sSummaryInfo.outputFilePath != null) {
|
||||
cacheSummary.outputFilePath = sSummaryInfo.outputFilePath
|
||||
}
|
||||
|
||||
sSummaryInfo.removedFileSet.each {
|
||||
cacheSummary.updateSdkMap.remove(it)
|
||||
cacheSummary.updateComponentMap.remove(it)
|
||||
}
|
||||
|
||||
sSummaryInfo.updateSdkMap.each {
|
||||
cacheSummary.updateSdkMap.put(it.key, it.value)
|
||||
}
|
||||
|
||||
sSummaryInfo.updateComponentMap.each {
|
||||
cacheSummary.updateComponentMap.put(it.key, it.value)
|
||||
}
|
||||
|
||||
cacheSummary.updateSdkMap.each {
|
||||
if (it != null && !it.value.isEmpty()) {
|
||||
sSdkInfo.addAll(it.value)
|
||||
}
|
||||
}
|
||||
|
||||
cacheSummary.updateComponentMap.each {
|
||||
if (it != null && !it.value.isEmpty()) {
|
||||
sComponentInfo.addAll(it.value)
|
||||
}
|
||||
}
|
||||
|
||||
return cacheSummary
|
||||
|
||||
}
|
||||
|
||||
static void clearSummaryInfo() {
|
||||
// TODO clear data
|
||||
sSummaryInfo = new ScanSummaryInfo()
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.plugin.component.transform.info;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* author : linzheng
|
||||
* e-mail : linzheng@corp.netease.com
|
||||
* time : 2019/10/22
|
||||
* desc :
|
||||
* version: 1.0
|
||||
*/
|
||||
public class ScanSummaryInfo {
|
||||
|
||||
|
||||
public String inputFilePath;
|
||||
|
||||
public String outputFilePath;
|
||||
|
||||
Map<String, Set<ScanSdkInfo>> updateSdkMap = new HashMap<>();
|
||||
|
||||
Map<String, Set<ScanComponentInfo>> updateComponentMap = new HashMap<>();
|
||||
|
||||
|
||||
Set<String> removedFileSet = new HashSet<>();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
package com.plugin.component.utils;
|
||||
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.ddmlib.Log;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.io.Serializable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* author: Blankj
|
||||
* blog : http://blankj.com
|
||||
* time : 2017/05/24
|
||||
* desc : utils about disk cache
|
||||
* </pre>
|
||||
*/
|
||||
public final class CacheDiskUtils {
|
||||
|
||||
private static final long DEFAULT_MAX_SIZE = Long.MAX_VALUE;
|
||||
private static final int DEFAULT_MAX_COUNT = Integer.MAX_VALUE;
|
||||
private static final String CACHE_PREFIX = "cdu_";
|
||||
private static final String TYPE_BYTE = "by_";
|
||||
private static final String TYPE_STRING = "st_";
|
||||
private static final String TYPE_JSON_OBJECT = "jo_";
|
||||
private static final String TYPE_JSON_ARRAY = "ja_";
|
||||
private static final String TYPE_BITMAP = "bi_";
|
||||
private static final String TYPE_DRAWABLE = "dr_";
|
||||
private static final String TYPE_PARCELABLE = "pa_";
|
||||
private static final String TYPE_SERIALIZABLE = "se_";
|
||||
|
||||
private static final Map<String, CacheDiskUtils> CACHE_MAP = new HashMap<>();
|
||||
|
||||
private final String mCacheKey;
|
||||
private final File mCacheDir;
|
||||
private final long mMaxSize;
|
||||
private final int mMaxCount;
|
||||
private DiskCacheManager mDiskCacheManager;
|
||||
|
||||
|
||||
/**
|
||||
* Return the single {@link CacheDiskUtils} instance.
|
||||
* <p>cache size: unlimited</p>
|
||||
* <p>cache count: unlimited</p>
|
||||
*
|
||||
* @param cacheDir The directory of cache.
|
||||
* @return the single {@link CacheDiskUtils} instance
|
||||
*/
|
||||
public static CacheDiskUtils getInstance(@NonNull final File cacheDir) {
|
||||
return getInstance(cacheDir, DEFAULT_MAX_SIZE, DEFAULT_MAX_COUNT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the single {@link CacheDiskUtils} instance.
|
||||
*
|
||||
* @param cacheDir The directory of cache.
|
||||
* @param maxSize The max size of cache, in bytes.
|
||||
* @param maxCount The max count of cache.
|
||||
* @return the single {@link CacheDiskUtils} instance
|
||||
*/
|
||||
public static CacheDiskUtils getInstance(@NonNull final File cacheDir,
|
||||
final long maxSize,
|
||||
final int maxCount) {
|
||||
final String cacheKey = cacheDir.getAbsoluteFile() + "_" + maxSize + "_" + maxCount;
|
||||
CacheDiskUtils cache = CACHE_MAP.get(cacheKey);
|
||||
if (cache == null) {
|
||||
synchronized (CacheDiskUtils.class) {
|
||||
cache = CACHE_MAP.get(cacheKey);
|
||||
if (cache == null) {
|
||||
cache = new CacheDiskUtils(cacheKey, cacheDir, maxSize, maxCount);
|
||||
CACHE_MAP.put(cacheKey, cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
private CacheDiskUtils(final String cacheKey,
|
||||
final File cacheDir,
|
||||
final long maxSize,
|
||||
final int maxCount) {
|
||||
mCacheKey = cacheKey;
|
||||
mCacheDir = cacheDir;
|
||||
mMaxSize = maxSize;
|
||||
mMaxCount = maxCount;
|
||||
}
|
||||
|
||||
private DiskCacheManager getDiskCacheManager() {
|
||||
if (mCacheDir.exists()) {
|
||||
if (mDiskCacheManager == null) {
|
||||
mDiskCacheManager = new DiskCacheManager(mCacheDir, mMaxSize, mMaxCount);
|
||||
}
|
||||
} else {
|
||||
if (mCacheDir.mkdirs()) {
|
||||
mDiskCacheManager = new DiskCacheManager(mCacheDir, mMaxSize, mMaxCount);
|
||||
} else {
|
||||
Log.e("CacheDiskUtils", "can't make dirs in " + mCacheDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
return mDiskCacheManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mCacheKey + "@" + Integer.toHexString(hashCode());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// about bytes
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Put bytes in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @param value The value of cache.
|
||||
*/
|
||||
public void put(@NonNull final String key, final byte[] value) {
|
||||
put(key, value, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put bytes in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @param value The value of cache.
|
||||
* @param saveTime The save time of cache, in seconds.
|
||||
*/
|
||||
public void put(@NonNull final String key, final byte[] value, final int saveTime) {
|
||||
realPutBytes(TYPE_BYTE + key, value, saveTime);
|
||||
}
|
||||
|
||||
private void realPutBytes(final String key, byte[] value, int saveTime) {
|
||||
if (value == null) return;
|
||||
DiskCacheManager diskCacheManager = getDiskCacheManager();
|
||||
if (diskCacheManager == null) return;
|
||||
if (saveTime >= 0) value = DiskCacheHelper.newByteArrayWithTime(saveTime, value);
|
||||
File file = diskCacheManager.getFileBeforePut(key);
|
||||
writeFileFromBytes(file, value);
|
||||
diskCacheManager.updateModify(file);
|
||||
diskCacheManager.put(file);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the bytes in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @return the bytes if cache exists or null otherwise
|
||||
*/
|
||||
public byte[] getBytes(@NonNull final String key) {
|
||||
return getBytes(key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bytes in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @param defaultValue The default value if the cache doesn't exist.
|
||||
* @return the bytes if cache exists or defaultValue otherwise
|
||||
*/
|
||||
public byte[] getBytes(@NonNull final String key, final byte[] defaultValue) {
|
||||
return realGetBytes(TYPE_BYTE + key, defaultValue);
|
||||
}
|
||||
|
||||
private byte[] realGetBytes(@NonNull final String key) {
|
||||
return realGetBytes(key, null);
|
||||
}
|
||||
|
||||
private byte[] realGetBytes(@NonNull final String key, final byte[] defaultValue) {
|
||||
DiskCacheManager diskCacheManager = getDiskCacheManager();
|
||||
if (diskCacheManager == null) return defaultValue;
|
||||
final File file = diskCacheManager.getFileIfExists(key);
|
||||
if (file == null) return defaultValue;
|
||||
byte[] data = readFile2Bytes(file);
|
||||
if (DiskCacheHelper.isDue(data)) {
|
||||
diskCacheManager.removeByKey(key);
|
||||
return defaultValue;
|
||||
}
|
||||
diskCacheManager.updateModify(file);
|
||||
return DiskCacheHelper.getDataWithoutDueTime(data);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// about String
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Put string value in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @param value The value of cache.
|
||||
*/
|
||||
public void put(@NonNull final String key, final String value) {
|
||||
put(key, value, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put string value in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @param value The value of cache.
|
||||
* @param saveTime The save time of cache, in seconds.
|
||||
*/
|
||||
public void put(@NonNull final String key, final String value, final int saveTime) {
|
||||
realPutBytes(TYPE_STRING + key, string2Bytes(value), saveTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string value in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @return the string value if cache exists or null otherwise
|
||||
*/
|
||||
public String getString(@NonNull final String key) {
|
||||
return getString(key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string value in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @param defaultValue The default value if the cache doesn't exist.
|
||||
* @return the string value if cache exists or defaultValue otherwise
|
||||
*/
|
||||
public String getString(@NonNull final String key, final String defaultValue) {
|
||||
byte[] bytes = realGetBytes(TYPE_STRING + key);
|
||||
if (bytes == null) return defaultValue;
|
||||
return bytes2String(bytes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Put serializable in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @param value The value of cache.
|
||||
*/
|
||||
public void put(@NonNull final String key, final Serializable value) {
|
||||
put(key, value, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put serializable in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @param value The value of cache.
|
||||
* @param saveTime The save time of cache, in seconds.
|
||||
*/
|
||||
public void put(@NonNull final String key, final Serializable value, final int saveTime) {
|
||||
realPutBytes(TYPE_SERIALIZABLE + key, serializable2Bytes(value), saveTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the serializable in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @return the bitmap if cache exists or null otherwise
|
||||
*/
|
||||
public Object getSerializable(@NonNull final String key) {
|
||||
return getSerializable(key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the serializable in cache.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @param defaultValue The default value if the cache doesn't exist.
|
||||
* @return the bitmap if cache exists or defaultValue otherwise
|
||||
*/
|
||||
public Object getSerializable(@NonNull final String key, final Object defaultValue) {
|
||||
byte[] bytes = realGetBytes(TYPE_SERIALIZABLE + key);
|
||||
if (bytes == null) return defaultValue;
|
||||
return bytes2Object(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the size of cache, in bytes.
|
||||
*
|
||||
* @return the size of cache, in bytes
|
||||
*/
|
||||
public long getCacheSize() {
|
||||
DiskCacheManager diskCacheManager = getDiskCacheManager();
|
||||
if (diskCacheManager == null) return 0;
|
||||
return diskCacheManager.getCacheSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the count of cache.
|
||||
*
|
||||
* @return the count of cache
|
||||
*/
|
||||
public int getCacheCount() {
|
||||
DiskCacheManager diskCacheManager = getDiskCacheManager();
|
||||
if (diskCacheManager == null) return 0;
|
||||
return diskCacheManager.getCacheCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the cache by key.
|
||||
*
|
||||
* @param key The key of cache.
|
||||
* @return {@code true}: success<br>{@code false}: fail
|
||||
*/
|
||||
public boolean remove(@NonNull final String key) {
|
||||
DiskCacheManager diskCacheManager = getDiskCacheManager();
|
||||
if (diskCacheManager == null) return true;
|
||||
return diskCacheManager.removeByKey(TYPE_BYTE + key)
|
||||
&& diskCacheManager.removeByKey(TYPE_STRING + key)
|
||||
&& diskCacheManager.removeByKey(TYPE_JSON_OBJECT + key)
|
||||
&& diskCacheManager.removeByKey(TYPE_JSON_ARRAY + key)
|
||||
&& diskCacheManager.removeByKey(TYPE_BITMAP + key)
|
||||
&& diskCacheManager.removeByKey(TYPE_DRAWABLE + key)
|
||||
&& diskCacheManager.removeByKey(TYPE_PARCELABLE + key)
|
||||
&& diskCacheManager.removeByKey(TYPE_SERIALIZABLE + key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all of the cache.
|
||||
*
|
||||
* @return {@code true}: success<br>{@code false}: fail
|
||||
*/
|
||||
public boolean clear() {
|
||||
DiskCacheManager diskCacheManager = getDiskCacheManager();
|
||||
if (diskCacheManager == null) return true;
|
||||
return diskCacheManager.clear();
|
||||
}
|
||||
|
||||
private static final class DiskCacheManager {
|
||||
private final AtomicLong cacheSize;
|
||||
private final AtomicInteger cacheCount;
|
||||
private final long sizeLimit;
|
||||
private final int countLimit;
|
||||
private final Map<File, Long> lastUsageDates
|
||||
= Collections.synchronizedMap(new HashMap<File, Long>());
|
||||
private final File cacheDir;
|
||||
private final Thread mThread;
|
||||
|
||||
private DiskCacheManager(final File cacheDir, final long sizeLimit, final int countLimit) {
|
||||
this.cacheDir = cacheDir;
|
||||
this.sizeLimit = sizeLimit;
|
||||
this.countLimit = countLimit;
|
||||
cacheSize = new AtomicLong();
|
||||
cacheCount = new AtomicInteger();
|
||||
mThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int size = 0;
|
||||
int count = 0;
|
||||
final File[] cachedFiles = cacheDir.listFiles(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
return name.startsWith(CACHE_PREFIX);
|
||||
}
|
||||
});
|
||||
if (cachedFiles != null) {
|
||||
for (File cachedFile : cachedFiles) {
|
||||
size += cachedFile.length();
|
||||
count += 1;
|
||||
lastUsageDates.put(cachedFile, cachedFile.lastModified());
|
||||
}
|
||||
cacheSize.getAndAdd(size);
|
||||
cacheCount.getAndAdd(count);
|
||||
}
|
||||
}
|
||||
});
|
||||
mThread.start();
|
||||
}
|
||||
|
||||
private long getCacheSize() {
|
||||
wait2InitOk();
|
||||
return cacheSize.get();
|
||||
}
|
||||
|
||||
private int getCacheCount() {
|
||||
wait2InitOk();
|
||||
return cacheCount.get();
|
||||
}
|
||||
|
||||
private File getFileBeforePut(final String key) {
|
||||
wait2InitOk();
|
||||
File file = new File(cacheDir, getCacheNameByKey(key));
|
||||
if (file.exists()) {
|
||||
cacheCount.addAndGet(-1);
|
||||
cacheSize.addAndGet(-file.length());
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private void wait2InitOk() {
|
||||
try {
|
||||
mThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private File getFileIfExists(final String key) {
|
||||
File file = new File(cacheDir, getCacheNameByKey(key));
|
||||
if (!file.exists()) return null;
|
||||
return file;
|
||||
}
|
||||
|
||||
private String getCacheNameByKey(final String key) {
|
||||
return CACHE_PREFIX + key.substring(0, 3) + key.substring(3).hashCode();
|
||||
}
|
||||
|
||||
private void put(final File file) {
|
||||
cacheCount.addAndGet(1);
|
||||
cacheSize.addAndGet(file.length());
|
||||
while (cacheCount.get() > countLimit || cacheSize.get() > sizeLimit) {
|
||||
cacheSize.addAndGet(-removeOldest());
|
||||
cacheCount.addAndGet(-1);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateModify(final File file) {
|
||||
Long millis = System.currentTimeMillis();
|
||||
file.setLastModified(millis);
|
||||
lastUsageDates.put(file, millis);
|
||||
}
|
||||
|
||||
private boolean removeByKey(final String key) {
|
||||
File file = getFileIfExists(key);
|
||||
if (file == null) return true;
|
||||
if (!file.delete()) return false;
|
||||
cacheSize.addAndGet(-file.length());
|
||||
cacheCount.addAndGet(-1);
|
||||
lastUsageDates.remove(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clear() {
|
||||
File[] files = cacheDir.listFiles(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
return name.startsWith(CACHE_PREFIX);
|
||||
}
|
||||
});
|
||||
if (files == null || files.length <= 0) return true;
|
||||
boolean flag = true;
|
||||
for (File file : files) {
|
||||
if (!file.delete()) {
|
||||
flag = false;
|
||||
continue;
|
||||
}
|
||||
cacheSize.addAndGet(-file.length());
|
||||
cacheCount.addAndGet(-1);
|
||||
lastUsageDates.remove(file);
|
||||
}
|
||||
if (flag) {
|
||||
lastUsageDates.clear();
|
||||
cacheSize.set(0);
|
||||
cacheCount.set(0);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the oldest files.
|
||||
*
|
||||
* @return the size of oldest files, in bytes
|
||||
*/
|
||||
private long removeOldest() {
|
||||
if (lastUsageDates.isEmpty()) return 0;
|
||||
Long oldestUsage = Long.MAX_VALUE;
|
||||
File oldestFile = null;
|
||||
Set<Map.Entry<File, Long>> entries = lastUsageDates.entrySet();
|
||||
synchronized (lastUsageDates) {
|
||||
for (Map.Entry<File, Long> entry : entries) {
|
||||
Long lastValueUsage = entry.getValue();
|
||||
if (lastValueUsage < oldestUsage) {
|
||||
oldestUsage = lastValueUsage;
|
||||
oldestFile = entry.getKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldestFile == null) return 0;
|
||||
long fileSize = oldestFile.length();
|
||||
if (oldestFile.delete()) {
|
||||
lastUsageDates.remove(oldestFile);
|
||||
return fileSize;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DiskCacheHelper {
|
||||
|
||||
static final int TIME_INFO_LEN = 14;
|
||||
|
||||
private static byte[] newByteArrayWithTime(final int second, final byte[] data) {
|
||||
byte[] time = createDueTime(second).getBytes();
|
||||
byte[] content = new byte[time.length + data.length];
|
||||
System.arraycopy(time, 0, content, 0, time.length);
|
||||
System.arraycopy(data, 0, content, time.length, data.length);
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string of due time.
|
||||
*
|
||||
* @param seconds The seconds.
|
||||
* @return the string of due time
|
||||
*/
|
||||
private static String createDueTime(final int seconds) {
|
||||
return String.format(
|
||||
Locale.getDefault(), "_$%010d$_",
|
||||
System.currentTimeMillis() / 1000 + seconds
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean isDue(final byte[] data) {
|
||||
long millis = getDueTime(data);
|
||||
return millis != -1 && System.currentTimeMillis() > millis;
|
||||
}
|
||||
|
||||
private static long getDueTime(final byte[] data) {
|
||||
if (hasTimeInfo(data)) {
|
||||
String millis = new String(copyOfRange(data, 2, 12));
|
||||
try {
|
||||
return Long.parseLong(millis) * 1000;
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static byte[] getDataWithoutDueTime(final byte[] data) {
|
||||
if (hasTimeInfo(data)) {
|
||||
return copyOfRange(data, TIME_INFO_LEN, data.length);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private static byte[] copyOfRange(final byte[] original, final int from, final int to) {
|
||||
int newLength = to - from;
|
||||
if (newLength < 0) throw new IllegalArgumentException(from + " > " + to);
|
||||
byte[] copy = new byte[newLength];
|
||||
System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength));
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static boolean hasTimeInfo(final byte[] data) {
|
||||
return data != null
|
||||
&& data.length >= TIME_INFO_LEN
|
||||
&& data[0] == '_'
|
||||
&& data[1] == '$'
|
||||
&& data[12] == '$'
|
||||
&& data[13] == '_';
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// other utils methods
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static byte[] string2Bytes(final String string) {
|
||||
if (string == null) return null;
|
||||
return string.getBytes();
|
||||
}
|
||||
|
||||
private static String bytes2String(final byte[] bytes) {
|
||||
if (bytes == null) return null;
|
||||
return new String(bytes);
|
||||
}
|
||||
|
||||
private static byte[] serializable2Bytes(final Serializable serializable) {
|
||||
if (serializable == null) return null;
|
||||
ByteArrayOutputStream baos;
|
||||
ObjectOutputStream oos = null;
|
||||
try {
|
||||
oos = new ObjectOutputStream(baos = new ByteArrayOutputStream());
|
||||
oos.writeObject(serializable);
|
||||
return baos.toByteArray();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
if (oos != null) {
|
||||
oos.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Object bytes2Object(final byte[] bytes) {
|
||||
if (bytes == null) return null;
|
||||
ObjectInputStream ois = null;
|
||||
try {
|
||||
ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
|
||||
return ois.readObject();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
if (ois != null) {
|
||||
ois.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void writeFileFromBytes(final File file, final byte[] bytes) {
|
||||
FileOutputStream outputStream = null;
|
||||
FileChannel fc = null;
|
||||
try {
|
||||
outputStream = new FileOutputStream(file, false);
|
||||
fc = outputStream.getChannel();
|
||||
fc.write(ByteBuffer.wrap(bytes));
|
||||
fc.force(true);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (fc != null) {
|
||||
fc.close();
|
||||
}
|
||||
if (outputStream != null) {
|
||||
outputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readFile2Bytes(final File file) {
|
||||
RandomAccessFile accessFile = null;
|
||||
FileChannel fc = null;
|
||||
FileInputStream inputStream = null;
|
||||
try {
|
||||
accessFile = new RandomAccessFile(file, "r");
|
||||
fc = accessFile.getChannel();
|
||||
int size = (int) fc.size();
|
||||
byte[] data = new byte[size];
|
||||
fc.read(ByteBuffer.wrap(data));
|
||||
return data;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
if (fc != null) {
|
||||
fc.close();
|
||||
}
|
||||
if (accessFile != null) {
|
||||
accessFile.close();
|
||||
}
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSpace(final String s) {
|
||||
if (s == null) return true;
|
||||
for (int i = 0, len = s.length(); i < len; ++i) {
|
||||
if (!Character.isWhitespace(s.charAt(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user