Android zip详解
一:解析Zip
有两种方法一种是Android Open Source Project 开源的util 可以不用解压解析zip
package sw.superb.collage2.utils;
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.res.AssetFileDescriptor;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Collection;
import java.util.HashMap;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class ZipResourceFile {
//
// Read-only access to Zip archives, with minimal heap allocation.
//
static final String LOG_TAG = "zipro";
static final boolean LOGV = false;
private static final String TAG = "ZipResourceFile";
// 4-byte number
static private int swapEndian(int i)
{
return ((i & 0xff) << 24) + ((i & 0xff00) << 8) + ((i & 0xff0000) >>> 8)
+ ((i >>> 24) & 0xff);
}
// 2-byte number
static private int swapEndian(short i)
{
return ((i & 0x00FF) << 8 | (i & 0xFF00) >>> 8);
}
/*
* Zip file constants.
*/
static final int kEOCDSignature = 0x06054b50;
static final int kEOCDLen = 22;
static final int kEOCDNumEntries = 8; // offset to #of entries in file
static final int kEOCDSize = 12; // size of the central directory
static final int kEOCDFileOffset = 16; // offset to central directory
static final int kMaxCommentLen = 65535; // longest possible in ushort
static final int kMaxEOCDSearch = (kMaxCommentLen + kEOCDLen);
static final int kLFHSignature = 0x04034b50;
static final int kLFHLen = 30; // excluding variable-len fields
static final int kLFHNameLen = 26; // offset to filename length
static final int kLFHExtraLen = 28; // offset to extra length
static final int kCDESignature = 0x02014b50;
static final int kCDELen = 46; // excluding variable-len fields
static final int kCDEMethod = 10; // offset to compression method
static final int kCDEModWhen = 12; // offset to modification timestamp
static final int kCDECRC = 16; // offset to entry CRC
static final int kCDECompLen = 20; // offset to compressed length
static final int kCDEUncompLen = 24; // offset to uncompressed length
static final int kCDENameLen = 28; // offset to filename length
static final int kCDEExtraLen = 30; // offset to extra length
static final int kCDECommentLen = 32; // offset to comment length
static final int kCDELocalOffset = 42; // offset to local hdr
static final int kCompressStored = 0; // no compression
static final int kCompressDeflated = 8; // standard deflate
/*
* The values we return for ZipEntryRO use 0 as an invalid value, so we want
* to adjust the hash table index by a fixed amount. Using a large value
* helps insure that people don't mix & match arguments, e.g. to
* findEntryByIndex().
*/
static final int kZipEntryAdj = 10000;
static public final class ZipEntryRO {
public ZipEntryRO(final String zipFileName, final File file, final String fileName) {
mFileName = fileName;
mZipFileName = zipFileName;
mFile = file;
}
public final File mFile;
public final String mFileName;
public final String mZipFileName;
public long mLocalHdrOffset; // offset of local file header
/* useful stuff from the directory entry */
public int mMethod;
public long mWhenModified;
public long mCRC32;
public long mCompressedLength;
public long mUncompressedLength;
public long mOffset = -1;
public void setOffsetFromFile(RandomAccessFile f, ByteBuffer buf) throws IOException {
long localHdrOffset = mLocalHdrOffset;
try {
f.seek(localHdrOffset);
f.readFully(buf.array());
if (buf.getInt(0) != kLFHSignature) {
Log.w(LOG_TAG, "didn't find signature at start of lfh");
throw new IOException();
}
int nameLen = buf.getShort(kLFHNameLen) & 0xFFFF;
int extraLen = buf.getShort(kLFHExtraLen) & 0xFFFF;
mOffset = localHdrOffset + kLFHLen + nameLen + extraLen;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* Calculates the offset of the start of the Zip file entry within the
* Zip file.
*
* @return the offset, in bytes from the start of the file of the entry
*/
public long getOffset() {
return mOffset;
}
/**
* isUncompressed
*
* @return true if the file is stored in uncompressed form
*/
public boolean isUncompressed() {
return mMethod == kCompressStored;
}
public AssetFileDescriptor getAssetFileDescriptor() {
if (mMethod == kCompressStored) {
ParcelFileDescriptor pfd;
try {
pfd = ParcelFileDescriptor.open(mFile, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, getOffset(), mUncompressedLength);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}
public String getZipFileName() {
return mZipFileName;
}
public File getZipFile() {
return mFile;
}
}
public HashMap<String, ZipEntryRO> mHashMap = new HashMap<String, ZipEntryRO>();
/* for reading compressed files */
public HashMap<File, ZipFile> mZipFiles = new HashMap<File, ZipFile>();
public ZipResourceFile(String zipFileName) throws IOException {
addPatchFile(zipFileName);
}
ZipEntryRO[] getEntriesAt(String path) {
Vector<ZipEntryRO> zev = new Vector<ZipEntryRO>();
Collection<ZipEntryRO> values = mHashMap.values();
if (null == path)
path = "";
int length = path.length();
for (ZipEntryRO ze : values) {
if (ze.mFileName.startsWith(path)) {
if (-1 == ze.mFileName.indexOf('/', length)) {
zev.add(ze);
}
}
}
ZipEntryRO[] entries = new ZipEntryRO[zev.size()];
return zev.toArray(entries);
}
public ZipEntryRO[] getAllEntries() {
Collection<ZipEntryRO> values = mHashMap.values();
return values.toArray(new ZipEntryRO[values.size()]);
}
/**
* getAssetFileDescriptor allows for ZipResourceFile to directly feed
* Android API's that want an fd, offset, and length such as the
* MediaPlayer. It also allows for the class to be used in a content
* provider that can feed video players. The file must be stored
* (non-compressed) in the Zip file for this to work.
*
* @param assetPath
* @return the asset file descriptor for the file, or null if the file isn't
* present or is stored compressed
*/
public AssetFileDescriptor getAssetFileDescriptor(String assetPath) {
ZipEntryRO entry = mHashMap.get(assetPath);
if (null != entry) {
return entry.getAssetFileDescriptor();
}
return null;
}
/**
* getInputStream returns an AssetFileDescriptor.AutoCloseInputStream
* associated with the asset that is contained in the Zip file, or a
* standard ZipInputStream if necessary to uncompress the file
*
* @param assetPath
* @return an input stream for the named asset path, or null if not found
* @throws IOException
*/
public InputStream getInputStream(String assetPath) throws IOException {
ZipEntryRO entry = mHashMap.get(assetPath);
if (null != entry) {
if (entry.isUncompressed()) {
return entry.getAssetFileDescriptor().createInputStream();
} else {
ZipFile zf = mZipFiles.get(entry.getZipFile());
/** read compressed files **/
if (null == zf) {
zf = new ZipFile(entry.getZipFile(), ZipFile.OPEN_READ);
mZipFiles.put(entry.getZipFile(), zf);
}
ZipEntry zi = zf.getEntry(assetPath);
if (null != zi)
return zf.getInputStream(zi);
}
}
return null;
}
ByteBuffer mLEByteBuffer = ByteBuffer.allocate(4);
static private int read4LE(RandomAccessFile f) throws EOFException, IOException {
return swapEndian(f.readInt());
}
/*
* Opens the specified file read-only. We memory-map the entire thing and
* close the file before returning.
*/
void addPatchFile(String zipFileName) throws IOException
{
File file = new File(zipFileName);
RandomAccessFile f = new RandomAccessFile(file, "r");
long fileLength = f.length();
if (fileLength < kEOCDLen) {
throw new IOException();
}
long readAmount = kMaxEOCDSearch;
if (readAmount > fileLength)
readAmount = fileLength;
/*
* Make sure this is a Zip archive.
*/
f.seek(0);
int header = read4LE(f);
if (header == kEOCDSignature) {
Log.i(LOG_TAG, "Found Zip archive, but it looks empty");
throw new IOException();
} else if (header != kLFHSignature) {
Log.v(LOG_TAG, "Not a Zip archive");
throw new IOException();
}
/*
* Perform the traditional EOCD snipe hunt. We're searching for the End
* of Central Directory magic number, which appears at the start of the
* EOCD block. It's followed by 18 bytes of EOCD stuff and up to 64KB of
* archive comment. We need to read the last part of the file into a
* buffer, dig through it to find the magic number, parse some values
* out, and use those to determine the extent of the CD. We start by
* pulling in the last part of the file.
*/
long searchStart = fileLength - readAmount;
f.seek(searchStart);
ByteBuffer bbuf = ByteBuffer.allocate((int) readAmount);
byte[] buffer = bbuf.array();
f.readFully(buffer);
bbuf.order(ByteOrder.LITTLE_ENDIAN);
/*
* Scan backward for the EOCD magic. In an archive without a trailing
* comment, we'll find it on the first try. (We may want to consider
* doing an initial minimal read; if we don't find it, retry with a
* second read as above.)
*/
// EOCD == 0x50, 0x4b, 0x05, 0x06
int eocdIdx;
for (eocdIdx = buffer.length - kEOCDLen; eocdIdx >= 0; eocdIdx--) {
if (buffer[eocdIdx] == 0x50 && bbuf.getInt(eocdIdx) == kEOCDSignature)
{
if (LOGV) {
Log.v(LOG_TAG, "+++ Found EOCD at index: " + eocdIdx);
}
break;
}
}
if (eocdIdx < 0) {
Log.d(LOG_TAG, "Zip: EOCD not found, " + zipFileName + " is not zip");
}
/*
* Grab the CD offset and size, and the number of entries in the
* archive. After that, we can release our EOCD hunt buffer.
*/
int numEntries = bbuf.getShort(eocdIdx + kEOCDNumEntries);
long dirSize = bbuf.getInt(eocdIdx + kEOCDSize) & 0xffffffffL;
long dirOffset = bbuf.getInt(eocdIdx + kEOCDFileOffset) & 0xffffffffL;
// Verify that they look reasonable.
if (dirOffset + dirSize > fileLength) {
Log.w(LOG_TAG, "bad offsets (dir " + dirOffset + ", size " + dirSize + ", eocd "
+ eocdIdx + ")");
throw new IOException();
}
if (numEntries == 0) {
Log.w(LOG_TAG, "empty archive?");
throw new IOException();
}
if (LOGV) {
Log.v(LOG_TAG, "+++ numEntries=" + numEntries + " dirSize=" + dirSize + " dirOffset="
+ dirOffset);
}
MappedByteBuffer directoryMap = f.getChannel()
.map(FileChannel.MapMode.READ_ONLY, dirOffset, dirSize);
directoryMap.order(ByteOrder.LITTLE_ENDIAN);
byte[] tempBuf = new byte[0xffff];
/*
* Walk through the central directory, adding entries to the hash table.
*/
int currentOffset = 0;
/*
* Allocate the local directory information
*/
ByteBuffer buf = ByteBuffer.allocate(kLFHLen);
buf.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < numEntries; i++) {
if (directoryMap.getInt(currentOffset) != kCDESignature) {
Log.w(LOG_TAG, "Missed a central dir sig (at " + currentOffset + ")");
throw new IOException();
}
/* useful stuff from the directory entry */
int fileNameLen = directoryMap.getShort(currentOffset + kCDENameLen) & 0xffff;
int extraLen = directoryMap.getShort(currentOffset + kCDEExtraLen) & 0xffff;
int commentLen = directoryMap.getShort(currentOffset + kCDECommentLen) & 0xffff;
/* get the CDE filename */
directoryMap.position(currentOffset + kCDELen);
directoryMap.get(tempBuf, 0, fileNameLen);
directoryMap.position(0);
/* UTF-8 on Android */
String str = new String(tempBuf, 0, fileNameLen);
if (LOGV) {
Log.v(LOG_TAG, "Filename: " + str);
}
ZipEntryRO ze = new ZipEntryRO(zipFileName, file, str);
ze.mMethod = directoryMap.getShort(currentOffset + kCDEMethod) & 0xffff;
ze.mWhenModified = directoryMap.getInt(currentOffset + kCDEModWhen) & 0xffffffffL;
ze.mCRC32 = directoryMap.getLong(currentOffset + kCDECRC) & 0xffffffffL;
ze.mCompressedLength = directoryMap.getLong(currentOffset + kCDECompLen) & 0xffffffffL;
ze.mUncompressedLength = directoryMap.getLong(currentOffset + kCDEUncompLen) & 0xffffffffL;
ze.mLocalHdrOffset = directoryMap.getInt(currentOffset + kCDELocalOffset) & 0xffffffffL;
// set the offsets
buf.clear();
ze.setOffsetFromFile(f, buf);
// put file into hash
mHashMap.put(str, ze);
// go to next directory entry
currentOffset += kCDELen + fileNameLen + extraLen + commentLen;
}
if (LOGV) {
Log.v(LOG_TAG, "+++ zip good scan " + numEntries + " entries");
}
}
}
package sw.superb.collage2.utils;
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Context;
import android.os.Environment;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
public class APKExpansionSupport {
// The shared path to all app expansion files
private final static String EXP_PATH = "/Android/obb/";
public static String[] getAPKExpansionFiles(Context ctx, int mainVersion, int patchVersion) {
String packageName = ctx.getPackageName();
Vector<String> ret = new Vector<String>();
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
// Build the full path to the app's expansion files
File root = Environment.getExternalStorageDirectory();
File expPath = new File(root.toString() + EXP_PATH + packageName);
// Check that expansion file path exists
if (expPath.exists()) {
if ( mainVersion > 0 ) {
String strMainPath = expPath + File.separator + "main." + mainVersion + "." + packageName + ".obb";
File main = new File(strMainPath);
if ( main.isFile() ) {
ret.add(strMainPath);
}
}
if ( patchVersion > 0 ) {
String strPatchPath = expPath + File.separator + "patch." + mainVersion + "." + packageName + ".obb";
File main = new File(strPatchPath);
if ( main.isFile() ) {
ret.add(strPatchPath);
}
}
}
}
String[] retArray = new String[ret.size()];
ret.toArray(retArray);
return retArray;
}
static public ZipResourceFile getResourceZipFile(String[] expansionFiles) throws IOException {
ZipResourceFile apkExpansionFile = null;
for (String expansionFilePath : expansionFiles) {
if ( null == apkExpansionFile ) {
apkExpansionFile = new ZipResourceFile(expansionFilePath);
} else {
apkExpansionFile.addPatchFile(expansionFilePath);
}
}
return apkExpansionFile;
}
static public ZipResourceFile getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion) throws IOException{
String[] expansionFiles = getAPKExpansionFiles(ctx, mainVersion, patchVersion);
return getResourceZipFile(expansionFiles);
}
public static String readDataFile(String file) throws Exception {
//截取路径的文件名 res
String fileName = file.substring(file.length() - 9, file.length() - 4);
ZipFile zf = new ZipFile(file);
InputStream in = new BufferedInputStream(new FileInputStream(file));
ZipInputStream zin = new ZipInputStream(in);
ZipEntry ze;
while ((ze = zin.getNextEntry()) != null) {
if (ze.isDirectory()) {
//Do nothing
} else {
if (ze.getName().equals(fileName + "describe")) {
BufferedReader br = new BufferedReader(
new InputStreamReader(zf.getInputStream(ze)));
String line;
while ((line = br.readLine()) != null) {
return line;
}
br.close();
}
}
}
zin.closeEntry();
return "";
}
}
另外一种可以通过zipFile和ZipOutputStream解析zip,ZipFile和ZipOutputStream解压zip的区别在于正常情况下都是用zipFile解压zip,但是在从网络中下载zip边下载边解压的时候是用ZipOutputStream是最理想的选择。这个就是解压缩zip文件的工具类
/**
* 解压zip到指定的路径
* @param zipFileString ZIP的名称
* @param outPathString 要解压缩路径
* @throws Exception
*/
public static void UnZipFolder(String zipFileString, String outPathString) throws Exception {
ZipInputStream inZip = new ZipInputStream(new FileInputStream(zipFileString));
ZipEntry zipEntry;
String szName = "";
while ((zipEntry = inZip.getNextEntry()) != null) {
szName = zipEntry.getName();
if (zipEntry.isDirectory()) {
//获取部件的文件夹名
szName = szName.substring(0, szName.length() - 1);
File folder = new File(outPathString + File.separator + szName);
folder.mkdirs();
} else {
Log.e(TAG,outPathString + File.separator + szName);
File file = new File(outPathString + File.separator + szName);
if (!file.exists()){
Log.e(TAG, "Create the file:" + outPathString + File.separator + szName);
file.getParentFile().mkdirs();
file.createNewFile();
}
// 获取文件的输出流
FileOutputStream out = new FileOutputStream(file);
int len;
byte[] buffer = new byte[1024];
// 读取(字节)字节到缓冲区
while ((len = inZip.read(buffer)) != -1) {
// 从缓冲区(0)位置写入(字节)字节
out.write(buffer, 0, len);
out.flush();
}
out.close();
}
}
inZip.close();
}
二:压缩zip
/**
* 压缩文件和文件夹
*
* @param srcFileString 要压缩的文件或文件夹
* @param zipFileString 压缩完成的Zip路径
* @throws Exception
* @return
*/
public static void ZipFolder(String srcFileString, String zipFileString) throws Exception {
//创建ZIP
ZipOutputStream outZip = new ZipOutputStream(new FileOutputStream(zipFileString));
//创建文件
File file = new File(srcFileString);
//压缩
ZipFiles(file.getParent()+ File.separator, file.getName(), outZip);
Log.e(TAG, "ZipFolderq: "+zipFileString );
//完成和关闭
outZip.finish();
outZip.close();
}
/**
* 压缩文件
*
* @param folderString
* @param fileString
* @param zipOutputSteam
* @throws Exception
*/
private static void ZipFiles(String folderString, String fileString, ZipOutputStream zipOutputSteam) throws Exception {
if (zipOutputSteam == null)
return;
File file = new File(folderString + fileString);
if (file.isFile()) {
ZipEntry zipEntry = new ZipEntry(fileString);
FileInputStream inputStream = new FileInputStream(file);
zipOutputSteam.putNextEntry(zipEntry);
int len;
byte[] buffer = new byte[4096];
while ((len = inputStream.read(buffer)) != -1) {
zipOutputSteam.write(buffer, 0, len);
}
zipOutputSteam.closeEntry();
} else {
//文件夹
String fileList[] = file.list();
//没有子文件和压缩
if (fileList.length <= 0) {
ZipEntry zipEntry = new ZipEntry(fileString + File.separator);
zipOutputSteam.putNextEntry(zipEntry);
zipOutputSteam.closeEntry();
}
//子文件和递归
for (int i = 0; i < fileList.length; i++) {
ZipFiles(folderString+fileString+"/", fileList[i], zipOutputSteam);
}
}
}
三:关于Android文件保存
在后面Android的版本中,一般给Android app一个单独的文件夹创建由开发者使用代码创建的File,具体目录结构为/android/data/你的应用的AppID/ 然后代码一般为
File file;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
file = new File(context.getExternalFilesDir(TypeName) + File.separator ,
"shapeframe"+Number);
} else {
file = new File(Environment.getExternalStorageDirectory() + "/Android",
"shapeframe"+Number);
}
在androidQ即以上使用 getExternalFilesDir得到这个目录,以下使用getExternalStorageDirectory,其中TypeName中带有"File.separator"可创建二级目录。