diff --git a/component-plugin/src/main/groovy/com/plugin/component/ComponentPlugin.groovy b/component-plugin/src/main/groovy/com/plugin/component/ComponentPlugin.groovy index 6fe8273..2c4841b 100644 --- a/component-plugin/src/main/groovy/com/plugin/component/ComponentPlugin.groovy +++ b/component-plugin/src/main/groovy/com/plugin/component/ComponentPlugin.groovy @@ -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 { 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 + "]注入插件 <=====") diff --git a/component-plugin/src/main/groovy/com/plugin/component/Constants.groovy b/component-plugin/src/main/groovy/com/plugin/component/Constants.groovy index 426b1e9..6de5c79 100644 --- a/component-plugin/src/main/groovy/com/plugin/component/Constants.groovy +++ b/component-plugin/src/main/groovy/com/plugin/component/Constants.groovy @@ -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" diff --git a/component-plugin/src/main/groovy/com/plugin/component/transform/CodeInjectProcessor.java b/component-plugin/src/main/groovy/com/plugin/component/transform/CodeInjectProcessor.java index 3ed36db..71ee1dc 100644 --- a/component-plugin/src/main/groovy/com/plugin/component/transform/CodeInjectProcessor.java +++ b/component-plugin/src/main/groovy/com/plugin/component/transform/CodeInjectProcessor.java @@ -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 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); - - } diff --git a/component-plugin/src/main/groovy/com/plugin/component/transform/ComponentTransform.java b/component-plugin/src/main/groovy/com/plugin/component/transform/ComponentTransform.java index 2127fe9..dfef2ee 100644 --- a/component-plugin/src/main/groovy/com/plugin/component/transform/ComponentTransform.java +++ b/component-plugin/src/main/groovy/com/plugin/component/transform/ComponentTransform.java @@ -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) { diff --git a/component-plugin/src/main/groovy/com/plugin/component/transform/ComponentWeaver.java b/component-plugin/src/main/groovy/com/plugin/component/transform/ComponentWeaver.java index b90f0f2..dc194c7 100644 --- a/component-plugin/src/main/groovy/com/plugin/component/transform/ComponentWeaver.java +++ b/component-plugin/src/main/groovy/com/plugin/component/transform/ComponentWeaver.java @@ -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 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; + } } diff --git a/component-plugin/src/main/groovy/com/plugin/component/transform/ScanCodeAdapter.groovy b/component-plugin/src/main/groovy/com/plugin/component/transform/ScanCodeAdapter.groovy index b2cdd2e..2fc9486 100644 --- a/component-plugin/src/main/groovy/com/plugin/component/transform/ScanCodeAdapter.groovy +++ b/component-plugin/src/main/groovy/com/plugin/component/transform/ScanCodeAdapter.groovy @@ -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() diff --git a/component-plugin/src/main/groovy/com/plugin/component/transform/info/ScanRuntime.groovy b/component-plugin/src/main/groovy/com/plugin/component/transform/info/ScanRuntime.groovy index dc75623..ab0b458 100644 --- a/component-plugin/src/main/groovy/com/plugin/component/transform/info/ScanRuntime.groovy +++ b/component-plugin/src/main/groovy/com/plugin/component/transform/info/ScanRuntime.groovy @@ -10,6 +10,9 @@ class ScanRuntime { static List sSdkInfo = new ArrayList<>() static List 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 getComponentSdkInfoList() { return componentSdkInfoList } + + + static void addSdkInfo(String filePath, @NonNull ScanSdkInfo scanSdkInfo) { + Set 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 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() + + } + + } diff --git a/component-plugin/src/main/groovy/com/plugin/component/transform/info/ScanSummaryInfo.java b/component-plugin/src/main/groovy/com/plugin/component/transform/info/ScanSummaryInfo.java new file mode 100644 index 0000000..81e06bb --- /dev/null +++ b/component-plugin/src/main/groovy/com/plugin/component/transform/info/ScanSummaryInfo.java @@ -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> updateSdkMap = new HashMap<>(); + + Map> updateComponentMap = new HashMap<>(); + + + Set removedFileSet = new HashSet<>(); + + + + + + + +} \ No newline at end of file diff --git a/component-plugin/src/main/groovy/com/plugin/component/utils/CacheDiskUtils.java b/component-plugin/src/main/groovy/com/plugin/component/utils/CacheDiskUtils.java new file mode 100644 index 0000000..6b9f90b --- /dev/null +++ b/component-plugin/src/main/groovy/com/plugin/component/utils/CacheDiskUtils.java @@ -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; + +/** + *
+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2017/05/24
+ *     desc  : utils about disk cache
+ * 
+ */ +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 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. + *

cache size: unlimited

+ *

cache count: unlimited

+ * + * @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
{@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
{@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 lastUsageDates + = Collections.synchronizedMap(new HashMap()); + 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> entries = lastUsageDates.entrySet(); + synchronized (lastUsageDates) { + for (Map.Entry 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; + } +}