合并Transform , 支持增量编译

This commit is contained in:
linzheng\n8383
2019-10-23 16:48:11 +08:00
parent e1c827c447
commit 07338e18f8
9 changed files with 901 additions and 48 deletions

View File

@@ -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 + "]注入插件 <=====")

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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()

View File

@@ -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()
}
}

View File

@@ -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<>();
}

View File

@@ -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;
}
}