2006年04月20日

[J2ME]跟我学制作Pak文件


作者:cleverpig



版权声明:本文可以自由转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
作者:cleverpig(http://blog.matrix.org.cn/page/cleverpig)
原文:http://www.matrix.org.cn/resource/article/43/43966_J2ME_Pak.html
关键字:pak,j2me,减肥


序言:

    由于前些时间,一些matrixer常问关于j2me中使用Pak文件的问题。本人虽学艺不深,但满怀热心的做了一番探索,现将制作Pak文件的看法和方法公布出来,大家多多提意见。

一、什么是Pak文件:

    Pak文件就是将多个文件打包为一个单独文件,在这个文件中保存着多个文件的数据,当然还有一些描述文件结构的数据。所以将“Pak”作为文件的后缀是一种常规的用法,大家可以自定义其它的文件后缀。

二、为什么使用Pak文件:

    由于MIDP对发布安装的j2me程序大小进行了限制,所以缩小发布程序就意味着能够提供更多的程序或者内容(如图片、音乐)给用户。而通过研究发现zip/jar算法对大文件的压缩率高于对等量的多个小文件的压缩率。

    当然还有其它方法,这里简单做一下讨论比如使用混淆器ProGuard的“-overloadaggressively”选项使jar文件缩小,但也会导致一些错误,因为这种方法生成jar中的class符合java byte code标准,但是与java语法相悖,严重的可能造成一些jre对Object的序列化错误。

    所以使用Pak方法将程序中要用到的资源(图片、音乐、文本)组合为单一文件是一个安全有效的方法。而且对于一些商用程序,完全可以在pak文件中对文件数据进行加密,很好的保护了作者和公司的权益。本人的sample中使用了简单的“加减法”加密,对于手机这类设备来讲是一个效率较高的选择。

三、Pak文件的结构:

    大家可以自己设计Pak文件结构,本人这里只是抛砖引玉的作个sample。下面就是本人设计的Pak文件结构:

PAK File Header:Pak文件的头部


  * 签名:6字节char数组
* 版本号:32位float
* 文件table数量:32位整数
* 密码行为:8位字节
* 密码:8位字节
* 文件唯一ID:10字节char数组
* 保留位:32位整数(4字节)



File Table:Pak文件中包含文件的列表,在一个Pak文件中一个被包含的文件对应一个File Table。


* 文件名:30字节char数组
* 文件大小:32位整型
* 文件在pak文件中的位移:32位整数



Concatenated File Data:按File Table的顺序连接在一起的文件数据。


* 文件数据



四、程序框架:
    
    说明:由于Pak文件的制作和使用分别要使用两个java应用领域:j2se和j2me,所以本人将PakUtil类制作了2个版本(j2se和j2me)。

    程序框架如下:
    1。PakHeader类,定义了Pak文件头。
    2。PakFileTable类,定义Pak文件table。
    3。PakUtil类(j2se版),具备两个功能:将多个png图片合成一个Pak文件,并使用简单的加减加密法对其进行加密;从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)或者写为文件。
       PakUtil类(j2me版),具备的功能:从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)。

五、PakHeader和PakFileTable类:
    
PakHeader.java:


package cn.org.matrix.gmatrix.gameLab.util.pak;

/**
* Pak文件头:
* 结构:
*   签名:6字节char数组
*   版本号:32位float
*   文件table数量:32位整数
*   密码行为:8位字节
*   密码:8位字节
*   文件唯一ID:10字节char数组
*   保留位:32位整数(4字节)
* @author cleverpig
*
*/
class PakHeader {
        //定义文件唯一ID长度
        public static final int UNIQUEID_LENGTH=10;
        //定义文件签名长度
        public static final int SIGNATURE_LENGTH=6;
        //定义加法运算
        public static final int ADDITION_CIPHERACTION=0;
        //定义减法运算
        public static final int SUBTRACT_CIHOERACTION=1;
        //文件签名
        private char[] signature=new char[SIGNATURE_LENGTH];
        //版本号
        private float version=0f;
        //文件table数量
        private long numFileTableEntries=0;
        //密码使用方法:在原数据上进行加法还是减法
        private byte cipherAction=ADDITION_CIPHERACTION;
        //密码值
        private byte cipherValue=0x00;
        //唯一ID
        private char[] uniqueID=new char[UNIQUEID_LENGTH];
        //保留的4字节
        private long reserved=0;
        
        public PakHeader(){        
        }
        
        /**
         * 构造方法
         * @param signature 签名
         * @param version 版本
         * @param numFileTableEntries 文件table数量
         * @param cipherAction 密码使用方法
         * @param cipherValue 密码值
         * @param uniqueID 唯一ID
         * @param reserved 保留的2字节
         */
        public PakHeader(char[] signature,float version,
                        long numFileTableEntries,byte cipherAction,
                        byte cipherValue,char[] uniqueID,long reserved){
                for(int i=0;i<SIGNATURE_LENGTH;this.signature[i]=signature[i],i++)
                        ;
                this.version=version;
                this.cipherAction=cipherAction;
                this.numFileTableEntries=numFileTableEntries;
                this.cipherValue=cipherValue;
                for(int i=0;i<UNIQUEID_LENGTH;this.uniqueID[i]=uniqueID[i],i++)
                        ;
                
                this.reserved=reserved;
        }
        
        public byte getCipherValue() {
                return cipherValue;
        }
        public void setCipherValue(byte cipherValue) {
                this.cipherValue = cipherValue;
        }
        public long getNumFileTableEntries() {
                return numFileTableEntries;
        }
        public void setNumFileTableEntries(long numFileTableEntries) {
                this.numFileTableEntries = numFileTableEntries;
        }
        public long getReserved() {
                return reserved;
        }
        public void setReserved(long reserved) {
                this.reserved = reserved;
        }
        public char[] getUniqueID() {
                return uniqueID;
        }
        public void setUniqueID(char[] uniqueID) {
                for(int i=0;i<UNIQUEID_LENGTH;this.uniqueID[i]=uniqueID[i],i++)
                        ;
        }
        public float getVersion() {
                return version;
        }
        public void setVersion(float version) {
                this.version = version;
        }
        public byte getCipherAction() {
                return cipherAction;
        }

        public void setCipherAction(byte cipherAction) {
                this.cipherAction = cipherAction;
        }

        public char[] getSignature() {
                return signature;
        }

        public void setSignature(char[] signature) {
                for(int i=0;i<SIGNATURE_LENGTH;this.signature[i] = signature[i],i++)
                        ;
        }
        
        /**
         * 返回PakHeader的大小
         * @return 返回PakHeader的大小
         */
        public static int size(){
                return SIGNATURE_LENGTH+4+4+1+1+UNIQUEID_LENGTH+4;
        }
        
        public String toString(){
                String result="";
                result+="\t签名:"+new String(this.signature).trim()
                        +"\t版本号:"+this.version
                        +"\t文件table数量:"+this.numFileTableEntries
                        +"\t密码行为:" +this.cipherAction
                        +"\t密码:"+this.cipherValue
                        +"\t文件唯一ID:"+new String(this.uniqueID).trim()
                        +"\t保留位:"+this.reserved;
                return result;
        }

}



PakFileTable.java


package cn.org.matrix.gmatrix.gameLab.util.pak;

/**
* Pak文件table类
* 文件table结构:
*         文件名:30字节char数组
*         文件大小:32位整型
*         文件在pak文件中的位移:32位整数
* @author cleverpig
*
*/
class PakFileTable {
        public static final int FILENAME_LENGTH=30;
        //文件名
        private char[] fileName=new char[FILENAME_LENGTH];
        //文件大小
        private long fileSize=0L;
        //文件在pak文件中的位移
        private long offSet=0L;
        
        public PakFileTable(){
        }
        
        /**
         * 构造方法
         * @param fileName 文件名
         * @param fileSize 文件大小
         * @param offSet 文件在Pak文件中的位移
         */
        public PakFileTable(char[] fileName,
                        long fileSize,long offSet){
                for(int i=0;i<FILENAME_LENGTH;this.fileName[i]=fileName[i],i++)
                        ;
                this.fileSize=fileSize;
                this.offSet=offSet;
        }
        
        public char[] getFileName() {
                return fileName;
        }
        public void setFileName(char[] fileName) {
                for(int i=0;i<fileName.length;this.fileName[i]=fileName[i],i++)
                        ;
        }
        public long getFileSize() {
                return fileSize;
        }
        public void setFileSize(long fileSize) {
                this.fileSize = fileSize;
        }
        public long getOffSet() {
                return offSet;
        }
        public void setOffSet(long offSet) {
                this.offSet = offSet;
        }
        /**
         * 返回文件Table的大小
         * @return 返回文件Table的大小
         */
        public static int size(){
                return FILENAME_LENGTH+4+4;
        }
        
        public String toString(){
                return "\t文件名:"+new String(this.fileName).trim()
                        +"\t文件大小:"+this.fileSize
                        +"\t文件位移:"+this.offSet;
        }
}



六、PakUtil类(j2se版):

PakUtil.java


package cn.org.matrix.gmatrix.gameLab.util.pak;

import java.io.*;
import java.util.Vector;
/**
* Pak工具类
* 功能:
* 1.将多个png图片合成一个Pak文件,并使用简单的加减加密法对其进行加密;
* 2.从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)或者写为文件
* @author cleverpig
*
*/
public class PakUtil {

        public PakUtil(){
        }
        
        /**
         * 返回文件长度
         * @param filePath 文件路径
         * @return 文件长度
         */
        private long getFileSize(String filePath){
                File file=new File(filePath);
                return file.length();
        }
        
        /**
         * 返回文件名
         * @param filePath 文件路径
         * @return 文件名
         */
        private String getFileName(String filePath){
                File file=new File(filePath);
                return file.getName();
        }
        
        /**
         * 计算文件位移的起始点
         * @return 文件位移的起始点
         */
        private long workOutOffsetStart(PakHeader header){
                //计算出文件头+文件table的长度
                return PakHeader.size()+header.getNumFileTableEntries()*PakFileTable.size();
        }
        
        /**
         * 计算文件位移
         * @param fileIndex 文件序号
         * @param lastFileOffset 上一个文件位移
         * @return  文件在pak文件中的位移
         */
        private long workOutNextOffset(long sourceFileSize,long lastFileOffset){
                return lastFileOffset+sourceFileSize;
        }
        
        /**
         * 生成文件table
         * @param sourceFileName 源文件名
         * @param sourceFileSize 源文件长度
         * @param currentFileOffset 当前文件位移
         * @return 生成的PakFileTable对象
         */
        private PakFileTable generateFileTable(String sourceFileName,
                        long sourceFileSize,long currentFileOffset){
                PakFileTable ft=new PakFileTable();
                ft.setFileName(sourceFileName.toCharArray());
                ft.setFileSize(sourceFileSize);
                ft.setOffSet(currentFileOffset);
                return ft;
        }
        
        /**
         * 将char字符数组写入到DataOutputStream中
         * @param toWriteCharArray 被写入的char数组
         * @param dos DataOutputStream
         * @throws Exception
         */
        private void writeCharArray(char[] toWriteCharArray,DataOutputStream dos) throws Exception{
                for(int i=0;i<toWriteCharArray.length;dos.writeChar(toWriteCharArray[i]),i++);
        }
        
        /**
         * 使用文件头中的密码对数据进行加密
         * @param buff 被加密的数据
         * @param buffLength 数据的长度
         * @param header 文件头
         */
        private void encryptBuff(byte[] buff,int buffLength,PakHeader header){
                for(int i=0;i<buffLength;i++){
                        switch(header.getCipherAction()){
                        case PakHeader.ADDITION_CIPHERACTION:
                                buff[i]+=header.getCipherValue();
                                break;
                        case PakHeader.SUBTRACT_CIHOERACTION:
                                buff[i]-=header.getCipherValue();
                                break;
                        }
                }
        }
        
        /**
         * 使用文件头中的密码对数据进行解密
         * @param buff 被解密的数据
         * @param buffLength 数据的长度
         * @param header 文件头
         */
        private void decryptBuff(byte[] buff,int buffLength,PakHeader header){
                for(int i=0;i<buffLength;i++){
                        switch(header.getCipherAction()){
                        case PakHeader.ADDITION_CIPHERACTION:
                                buff[i]-=header.getCipherValue();
                                break;
                        case PakHeader.SUBTRACT_CIHOERACTION:
                                buff[i]+=header.getCipherValue();
                                break;
                        }
                }
        }
        
        /**
         * 制作Pak文件
         * @param sourceFilePath 源文件路径数组
         * @param destinateFilePath 目的文件路径(Pak文件)
         * @param cipherAction 密码行为
         * @param cipherValue 密码
         * @throws Exception
         */
        public void makePakFile(String[] sourceFilePath,
                        String destinateFilePath,PakHeader header) throws Exception{
                
                PakFileTable[] fileTable=new PakFileTable[sourceFilePath.length];
                //计算文件位移起始点
                long fileOffset=workOutOffsetStart(header);
                //逐个建立文件table
                for(int i=0;i<sourceFilePath.length;i++){
                        String sourceFileName=getFileName(sourceFilePath[i]);
                        long sourceFileSize=getFileSize(sourceFilePath[i]);
                        PakFileTable ft=generateFileTable(sourceFileName,sourceFileSize,fileOffset);
                        //计算下一个文件位移
                        fileOffset=workOutNextOffset(sourceFileSize,fileOffset);
                        fileTable[i]=ft;
                }
                //写入文件头
                File wFile=new File(destinateFilePath);
                FileOutputStream fos=new FileOutputStream(wFile);
                DataOutputStream dos=new DataOutputStream(fos);
                writeCharArray(header.getSignature(),dos);
                dos.writeFloat(header.getVersion());
                dos.writeLong(header.getNumFileTableEntries());
                dos.writeByte(header.getCipherAction());
                dos.writeByte(header.getCipherValue());
                writeCharArray(header.getUniqueID(),dos);
                dos.writeLong(header.getReserved());
                //写入文件table
                for(int i=0;i<fileTable.length;i++){
                        writeCharArray(fileTable[i].getFileName(),dos);
                        dos.writeLong(fileTable[i].getFileSize());

                        dos.writeLong(fileTable[i].getOffSet());
                }
                //写入文件数据
                for(int i=0;i<fileTable.length;i++){
                        File ftFile=new File(sourceFilePath[i]);
                        FileInputStream ftFis=new FileInputStream(ftFile);
                        DataInputStream ftDis=new DataInputStream(ftFis);
                        byte[] buff=new byte[256];
                        int readLength=0;
                        while((readLength=ftDis.read(buff))!=-1){
                                encryptBuff(buff,readLength,header);
                                dos.write(buff,0,readLength);
                        }
                        ftDis.close();
                        ftFis.close();
                }
                dos.close();        
        }
        
        /**
         * 从DataInputStream读取char数组
         * @param dis DataInputStream
         * @param readLength 读取长度
         * @return char数组
         * @throws Exception
         */
        private char[] readCharArray(DataInputStream dis,int readLength) throws Exception{
                char[] readCharArray=new char[readLength];
                
                for(int i=0;i<readLength;i++){
                        readCharArray[i]=dis.readChar();
                }
                return readCharArray;
        }
        
        /**
         * 从PAK文件中读取文件头
         * @param dis DataInputStream
         * @return PakHeader
         * @throws Exception
         */
        private PakHeader readHeader(DataInputStream dis) throws Exception{
                PakHeader header=new PakHeader();
                char[] signature=readCharArray(dis,PakHeader.SIGNATURE_LENGTH);
                header.setSignature(signature);
                header.setVersion(dis.readFloat());
                header.setNumFileTableEntries(dis.readLong());
                header.setCipherAction(dis.readByte());
                header.setCipherValue(dis.readByte());
                char[] uniqueID=readCharArray(dis,PakHeader.UNIQUEID_LENGTH);
                header.setUniqueID(uniqueID);
                header.setReserved(dis.readLong());
                return header;
        }
        
        /**
         * 读取所有的文件table
         * @param dis DataInputStream
         * @param fileTableNumber 文件表总数
         * @return 文件table数组
         * @throws Exception
         */
        private PakFileTable[] readFileTable(DataInputStream dis,int fileTableNumber) throws Exception{
                PakFileTable[] fileTable=new PakFileTable[fileTableNumber];
                for(int i=0;i<fileTableNumber;i++){
                        PakFileTable ft=new PakFileTable();
                        ft.setFileName(readCharArray(dis,PakFileTable.FILENAME_LENGTH));
                        ft.setFileSize(dis.readLong());
                        ft.setOffSet(dis.readLong());
                        fileTable[i]=ft;
                }
                return fileTable;
        }
        
        /**
         * 从pak文件读取文件到byte数组
         * @param dis DataInputStream
         * @param fileTable PakFileTable
         * @return byte数组
         * @throws Exception
         */
        private byte[] readFileFromPak(DataInputStream dis,PakHeader header,PakFileTable fileTable) throws Exception{
                dis.skip(fileTable.getOffSet()-workOutOffsetStart(header));
                //
                int fileLength=(int)fileTable.getFileSize();
                byte[] fileBuff=new byte[fileLength];
                int readLength=dis.read(fileBuff,0,fileLength);
                if (readLength<fileLength){
                        System.out.println("读取数据长度不正确");
                        return null;
                }
                else{
                        decryptBuff(fileBuff,readLength,header);
                        return fileBuff;
                }
        }
        
        /**
         * 将buffer中的内容写入到文件
         * @param fileBuff 保存文件内容的buffer
         * @param fileName 文件名
         * @param extractDir 文件导出目录
         * @throws Exception
         */
        private void writeFileFromByteBuffer(byte[] fileBuff,String fileName,String extractDir) throws Exception{
                String extractFilePath=extractDir+fileName;
                File wFile=new File(extractFilePath);
                FileOutputStream fos=new FileOutputStream(wFile);
                DataOutputStream dos=new DataOutputStream(fos);
                dos.write(fileBuff);
                dos.close();
                fos.close();
        }
        
        /**
         * 从pak文件中取出指定的文件到byte数组,如果需要的话可以将byte数组写为文件
         * @param pakFilePath  pak文件路径
         * @param extractFileName pak文件中将要被取出的文件名
         * @param writeFile 是否需要将byte数组写为文件
         * @param extractDir 如果需要的话可以将byte数组写为文件,extractDir为取出数据被写的目录文件
         * @return byte数组
         * @throws Exception
         */
        public byte[] extractFileFromPak(String pakFilePath,
                        String extractFileName,boolean writeFile,String extractDir) throws Exception{
                File rFile=new File(pakFilePath);
                FileInputStream fis=new FileInputStream(rFile);
                DataInputStream dis=new DataInputStream(fis);
                PakHeader header=readHeader(dis);
                PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries());

                boolean find=false;
                int fileIndex=0;
                for(int i=0;i<fileTable.length;i++){
                        String fileName=new String(fileTable[i].getFileName()).trim();
                        if (fileName.equals(extractFileName)){
                                find=true;
                                fileIndex=i;
                                break;
                        }
                }
                if (find==false){
                        System.out.println("没有找到指定的文件");
                        return null;
                }
                else{
                        byte[] buff=readFileFromPak(dis,header,fileTable[fileIndex]);
                        if (writeFile){
                                writeFileFromByteBuffer(buff,extractFileName,extractDir);
                        }
                        else{
                                dis.close();
                                fis.close();
                        }
                        return buff;
                }
        }
        
        
        /**
         * 从pak文件中取出指定的Pak文件的信息
         * @param pakFilePath  pak文件路径
         * @return 装载文件头和文件table数组的Vector
         * @throws Exception
         */
        public Vector showPakFileInfo(String pakFilePath) throws Exception{
                File rFile=new File(pakFilePath);
                
                FileInputStream fis=new FileInputStream(rFile);
                DataInputStream dis=new DataInputStream(fis);
                
                PakHeader header=readHeader(dis);
                PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries());

                Vector result=new Vector();
                result.add(header);
                result.add(fileTable);
                return result;
        }
        
        public static void main(String[] argv) throws Exception{
                PakUtil pu=new PakUtil();
                
                //构造文件头
                char[] signature=new char[PakHeader.SIGNATURE_LENGTH];
                signature=new String("012345").toCharArray();
                char[] uniqueID=new char[PakHeader.UNIQUEID_LENGTH];
                uniqueID=new String("0123456789").toCharArray();
                PakHeader header=new PakHeader();
                header.setSignature(signature);
                header.setNumFileTableEntries(3);
                header.setCipherAction((byte)PakHeader.ADDITION_CIPHERACTION);
                header.setCipherValue((byte)0x0f);
                header.setUniqueID(uniqueID);
                header.setVersion(1.0f);
                header.setReserved(0L);
                
                String[] filePathArray={"F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\apple.png",
                                "F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\cushaw.png",
                                "F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\flash.png"};
                String extractFilePath="F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\test.pak";
                //制作Pak文件
                System.out.println("制作Pak文件...");
                pu.makePakFile(filePathArray,extractFilePath,header);
                System.out.println("制作Pak文件完成");
                
                //从Pak文件中取出所有的图片文件
                Vector pakInfo=pu.showPakFileInfo(extractFilePath);
                header=(PakHeader)pakInfo.elementAt(0);
                System.out.println("Pak文件信息:");
                System.out.println("文件头:");
                System.out.println(header);
                
                PakFileTable[] fileTable=(PakFileTable[])pakInfo.elementAt(1);
                for(int i=0;i<fileTable.length;i++){
                        System.out.println("文件table["+i+"]:");
                        System.out.println(fileTable[i]);
                }
                
                String restoreDir="F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\extract\\";
                String restoreFileName=null;
                byte[] fileBuff=null;
                for(int i=0;i<fileTable.length;i++){
                        restoreFileName=new String(fileTable[i].getFileName()).trim();
                        System.out.println("从Pak文件中取出"+restoreFileName+"文件...");
                        fileBuff=pu.extractFileFromPak(extractFilePath,restoreFileName,true,restoreDir);
                        System.out.println("从Pak文件中取出"+restoreFileName+"文件保存在"+restoreDir+"目录");
                }
        }
}



七、PakUtil类(j2me版):

PakUtil.java


package cn.org.matrix.gmatrix.gameLab.util.pak;

import java.io.*;
import java.util.Vector;
/**
* Pak工具类
* 功能:
* 从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)
* @author cleverpig
*
*/
public class PakUtil {
        
        public PakUtil(){
        }
        
        /**
         * 计算文件位移的起始点
         * @return 文件位移的起始点
         */
        private long workOutOffsetStart(PakHeader header){
                //计算出文件头+文件table的长度
                return PakHeader.size()+header.getNumFileTableEntries()*PakFileTable.size();
        }
        
        /**
         * 从DataInputStream读取char数组
         * @param dis DataInputStream
         * @param readLength 读取长度
         * @return char数组
         * @throws Exception
         */
        private char[] readCharArray(DataInputStream dis,int readLength) throws Exception{
                char[] readCharArray=new char[readLength];
                
                for(int i=0;i<readLength;i++){
                        readCharArray[i]=dis.readChar();
                }
                return readCharArray;
        }
        
        /**
         * 从PAK文件中读取文件头
         * @param dis DataInputStream
         * @return PakHeader
         * @throws Exception
         */
        private PakHeader readHeader(DataInputStream dis) throws Exception{
                PakHeader header=new PakHeader();
                char[] signature=readCharArray(dis,PakHeader.SIGNATURE_LENGTH);
                header.setSignature(signature);
                header.setVersion(dis.readFloat());
                header.setNumFileTableEntries(dis.readLong());
                header.setCipherAction(dis.readByte());
                header.setCipherValue(dis.readByte());
                char[] uniqueID=readCharArray(dis,PakHeader.UNIQUEID_LENGTH);
                header.setUniqueID(uniqueID);
                header.setReserved(dis.readLong());
                return header;
        }
        
        /**
         * 读取所有的文件table
         * @param dis DataInputStream
         * @param fileTableNumber 文件表总数
         * @return 文件table数组
         * @throws Exception
         */
        private PakFileTable[] readFileTable(DataInputStream dis,int fileTableNumber) throws Exception{
                PakFileTable[] fileTable=new PakFileTable[fileTableNumber];
                for(int i=0;i<fileTableNumber;i++){
                        PakFileTable ft=new PakFileTable();
                        ft.setFileName(readCharArray(dis,PakFileTable.FILENAME_LENGTH));
                        ft.setFileSize(dis.readLong());
                        ft.setOffSet(dis.readLong());
                        fileTable[i]=ft;
                }
                return fileTable;
        }
        
        /**
         * 从pak文件读取文件到byte数组
         * @param dis DataInputStream
         * @param fileTable PakFileTable
         * @return byte数组
         * @throws Exception
         */
        private byte[] readFileFromPak(DataInputStream dis,PakHeader header,PakFileTable fileTable) throws Exception{
                dis.skip(fileTable.getOffSet()-workOutOffsetStart(header));
                //
                int fileLength=(int)fileTable.getFileSize();
                byte[] fileBuff=new byte[fileLength];
                int readLength=dis.read(fileBuff,0,fileLength);
                if (readLength<fileLength){
                        System.out.println("读取数据长度不正确");
                        return null;
                }
                else{
                        decryptBuff(fileBuff,readLength,header);
                }
                return fileBuff;
        }
        
        /**
         * 使用文件头中的密码对数据进行解密
         * @param buff 被解密的数据
         * @param buffLength 数据的长度
         * @param header 文件头
         */
        private void decryptBuff(byte[] buff,int buffLength,PakHeader header){
                for(int i=0;i<buffLength;i++){
                        switch(header.getCipherAction()){
                        case PakHeader.ADDITION_CIPHERACTION:
                                buff[i]-=header.getCipherValue();
                                break;
                        case PakHeader.SUBTRACT_CIHOERACTION:
                                buff[i]+=header.getCipherValue();
                                break;
                        }
                }
        }
        
        /**
         * 从pak文件中取出指定的文件到byte数组
         * @param pakResourceURL  pak文件的资源路径
         * @param extractResourceName pak文件中将要被取出的文件名
         * @return byte数组
         * @throws Exception
         */
        public byte[] extractResourceFromPak(String pakResourceURL
                        ,String extractResourceName) throws Exception{
                InputStream is=this.getClass().getResourceAsStream(pakResourceURL);
                DataInputStream dis=new DataInputStream(is);
                PakHeader header=readHeader(dis);
//                System.out.println("文件头:");
//                System.out.println(header);
                PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries());
//                for(int i=0;i<fileTable.length;i++){
//                        System.out.println("文件table["+i+"]:");
//                        System.out.println(fileTable[i]);
//                }
                boolean find=false;
                int fileIndex=0;
                for(int i=0;i<fileTable.length;i++){
                        String fileName=new String(fileTable[i].getFileName()).trim();
                        if (fileName.equals(extractResourceName)){
                                find=true;
                                fileIndex=i;
                                break;
                        }
                }
                if (find==false){
                        System.out.println("没有找到指定的文件");
                        return null;
                }
                else{
                        byte[] buff=readFileFromPak(dis,header,fileTable[fileIndex]);
                        return buff;
                }
        }
        
        
        /**
         * 从pak文件中取出指定的Pak文件的信息
         * @param pakResourcePath  pak文件资源路径
         * @return 装载文件头和文件table数组的Vector
         * @throws Exception
         */
        public Vector showPakFileInfo(String pakResourcePath) throws Exception{
                InputStream is=this.getClass().getResourceAsStream(pakResourcePath);
                DataInputStream dis=new DataInputStream(is);
                
                PakHeader header=readHeader(dis);
                PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries());

                Vector result=new Vector();
                result.addElement(header);
                result.addElement(fileTable);
                return result;
        }
        
        public static void main(String[] argv) throws Exception{
                PakUtil pu=new PakUtil();
                String extractResourcePath="/test.pak";
                //从Pak文件中取出所有的图片文件
                Vector pakInfo=pu.showPakFileInfo(extractResourcePath);
                PakHeader header=(PakHeader)pakInfo.elementAt(0);
                System.out.println("Pak文件信息:");
                System.out.println("文件头:");
                System.out.println(header);
                
                PakFileTable[] fileTable=(PakFileTable[])pakInfo.elementAt(1);
                for(int i=0;i<fileTable.length;i++){
                        System.out.println("文件table["+i+"]:");
                        System.out.println(fileTable[i]);
                }
                
                String restoreFileName=null;
                byte[] fileBuff=null;
                for(int i=0;i<fileTable.length;i++){
                        restoreFileName=new String(fileTable[i].getFileName()).trim();
                        System.out.println("从Pak文件中取出"+restoreFileName+"文件数据...");
                        fileBuff=pu.extractResourceFromPak(extractResourcePath,restoreFileName);
                        System.out.println("从Pak文件中取出"+restoreFileName+"文件数据完成");
                }
        }
}



八、源代码使用简介:

    Pak过程:j2se版的PakUtil将testFiles目录中的三个png文件Pak成为test.pak文件。
    UnPak过程:j2se版的PakUtil将testFiles目录中test.pak文件释放到testFiles\extract目录下;j2me版的PakUtil从res目录中的test.pak文件读取出其中所包含的3个png文件数据并装入到byte数据,用来构造Image对象,大家请运行PakUtilTestMIDlet.java便可看到输出的信息。

九、源代码下载:

j2se版源代码[下载文件]
j2sme版源代码[下载文件]

资源
·CleverPig的blog:http://blog.matrix.org.cn/page/cleverpig
·Matrix-Java开发者社区:http://www.matrix.org.cn/
·ProGuard
·j2me.org上的Topic: Give me all your tricks for minimizing jar file size

2006年03月12日

控制图片的Alpha通道,比如

    

try {
            image=Image.createImage("/123.png");//载入原图
        }
        catch (IOException e) { }
        int[] argb=new int[image.getWidth()*image.getHeight()];//产生图片数据数组
        image.getRGB(argb,0,image.getWidth(),0,0,image.getWidth(),image.getHeight());//得到ARGB矩阵
        for(int i=0;i<argb.length;i++){
            argb[i]&=0xa0ffffff;//设置每个象素的alpha通道值为a0,Alpha通道的值为0–100,想要渐变,让它从0慢慢到100就OK了

imageChange=Image.createRGBImage(argb,image.getWidth(),image.getHeight(),true);//产生新的图片

}

try {
            image=Image.createImage("/123.png");//载入原图
        }
        catch (IOException e) { }
        int[] argb=new int[image.getWidth()*image.getHeight()];//产生图片数据数组
        image.getRGB(argb,0,image.getWidth(),0,0,image.getWidth(),image.getHeight());//得到ARGB矩阵
        for(int i=0;i<argb.length;i++){
            argb[i]&=0xa0ffffff;//设置每个象素的alpha通道值为a0,Alpha通道的值为0–100,想要渐变,让它从0慢慢到100就OK了

imageChange=Image.createRGBImage(argb,image.getWidth(),image.getHeight(),true);//产生新的图片

}

2006年03月04日
为什么很多游戏要加入Loading滚动条呢?加入Loading状态并不是为了使软件显得更专业美观,而是为了保证程序的运行内存不溢出。通常计算机/手机的存储系统分为:cup 的缓存,磁盘(或者手机中的存储用的的FLASH RAM或者其他类型的可以持久保存的存储系统),运行内存。我们知道通常NOKIA S40的heap size为200KB大小,而通常我们加入程序和3张128*128的图片之后内存就趋于崩溃了,再加入声音和地图,程序的运算内存就显得太不够了。一般来讲,很多游戏仅仅在运行的时候把所有的资源一次性读入heap memory这样,我们在模拟器看到程序运行的状况就非常接近崩溃的边缘,如果不小心加入了新的图片,可能就没有足够的运算内存了。

         我们如何解决heap size不够的事情呢?手机是不能够改变其heap size的,我们只有想办法控制heap memory的使用。最直观的做法就是:存储内存与运算内存的优化使用,当运算内存需要资源时从存储内存中调用,需要新的资源时,就把不需要的释放掉。下面我就结合一段代码解释我们是如何制作Loading状态的。

         众所周知,Java是内置多线程的,我们可以使用两个线程来解决loading的问题,一个读资源的线程,一个绘制资源的线程。程序代码:

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

/**
* Loading演示
* @author gaogao
* */
class MainCanvas
    extends Canvas
    implements Runnable {

//程序状态
  static final int LOADING = 0;
  static final int GAMEING = 1;

//程序状态控制器
  int state = LOADING;

//主线程
  Thread thread = null;
//是否loading完毕,
  boolean isLoaded = false;

//内部类,新开读取资源的 线程
  class Loading
      implements Runnable {
    //内线程
    Thread innerThread = null;

    public Loading() {
      innerThread = new Thread(this);
      innerThread.start();
    }

    int counter = 100;
    public void run() {
      //模拟读取资源
      //把下面的东西改成读取资源的代码即可
      while (counter > 0) {
        counter–;
        try {
          Thread.sleep(20);
        }
        catch (Exception ex) {}
      }
      //loading结束
      isLoaded = true;
    }
  }

  Loading loading = null;

  public MainCanvas() {
    loading = new Loading();
    thread = new Thread(this);
    thread.start();

  }

  int loadingCounter = 0;

//绘制..
  public void paint(Graphics g) {
    g.setColor(0);
    g.fillRect(0, 0, getWidth(), getHeight());
    switch (state) {
      case LOADING: {
        g.setColor(0XFFFFFF);

        g.drawString("LOADING" + ">>>>>".substring(0, loadingCounter),
                     getWidth() >> 1, getHeight() >> 1,
                     Graphics.HCENTER | Graphics.TOP);

        loadingCounter = ++loadingCounter % 5;

      }
      break;
      case GAMEING: {
        g.setColor(0XFFFFFF);
        g.drawString("GAME", getWidth() >> 1, getHeight() >> 1,
                     Graphics.HCENTER | Graphics.TOP);
      }
      break;
    }
  }

  public void run() {
    while (true) {
      try {
        Thread.sleep(100);
      }
      catch (Exception ex) {

      }
      if (isLoaded) {
        loading = null;
        state = GAMEING;
      }
      repaint(0, 0, getWidth(), getHeight());
      serviceRepaints();
    }
  }
}

public class Main
    extends MIDlet {
  MainCanvas mc;

  public void startApp() {

    if (mc == null) {
      mc = new MainCanvas();
      Display disp = Display.getDisplay(this);
      disp.setCurrent(mc);
    }
  }

  public void destroyApp(boolean bool) {}

  public void pauseApp() {}

2005年12月19日

java语言已经内置了多线程支持,所有实现Runnable接口的类都可被启动一个新线程,新线程会执行该实例的run()方法,当run()方法执行完毕后,线程就结束了。一旦一个线程执行完毕,这个实例就不能再重新启动,只能重新生成一个新实例,再启动一个新线程。

Thread类是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法:

Thread t = new Thread();
t.start();

start()方法是一个native方法,它将启动一个新线程,并执行run()方法。Thread类默认的run()方法什么也不做就退出了。注意:直接调用run()方法并不会启动一个新线程,它和调用一个普通的java方法没有什么区别。

因此,有两个方法可以实现自己的线程:

方法1:自己的类extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:

public class MyThread extends Thread {
    public run() {
        System.out.println("MyThread.run()");
    }
}

在合适的地方启动线程:new MyThread().start();

方法2:如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口:

public class MyThread extends OtherClass implements Runnable {
    public run() {
        System.out.println("MyThread.run()");
    }
}

为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

MyThread myt = new MyThread();
Thread t = new Thread(myt);
t.start();

事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

public void run() {
    if (target != null) {
        target.run();
    }
}

线程还有一些Name, ThreadGroup, isDaemon等设置,由于和线程设计模式关联很少,这里就不多说了。

由于同一进程内的多个线程共享内存空间,在Java中,就是共享实例,当多个线程试图同时修改某个实例的内容时,就会造成冲突,因此,线程必须实现共享互斥,使多线程同步。

最简单的同步是将一个方法标记为synchronized,对同一个实例来说,任一时刻只能有一个synchronized方法在执行。当一个方法正在执行某个synchronized方法时,其他线程如果想要执行这个实例的任意一个synchronized方法,都必须等待当前执行 synchronized方法的线程退出此方法后,才能依次执行。

但是,非synchronized方法不受影响,不管当前有没有执行synchronized方法,非synchronized方法都可以被多个线程同时执行。

此外,必须注意,只有同一实例的synchronized方法同一时间只能被一个线程执行,不同实例的synchronized方法是可以并发的。例如,class A定义了synchronized方法sync(),则不同实例a1.sync()和a2.sync()可以同时由两个线程来执行。

多线程同步的实现最终依赖锁机制。我们可以想象某一共享资源是一间屋子,每个人都是一个线程。当A希望进入房间时,他必须获得门锁,一旦A获得门锁,他进去后就立刻将门锁上,于是B,C,D…就不得不在门外等待,直到A释放锁出来后,B,C,D…中的某一人抢到了该锁(具体抢法依赖于 JVM的实现,可以先到先得,也可以随机挑选),然后进屋又将门锁上。这样,任一时刻最多有一人在屋内(使用共享资源)。

Java语言规范内置了对多线程的支持。对于Java程序来说,每一个对象实例都有一把“锁”,一旦某个线程获得了该锁,别的线程如果希望获得该锁,只能等待这个线程释放锁之后。获得锁的方法只有一个,就是synchronized关键字。例如:

public class SharedResource {
    private int count = 0;

    public int getCount() { return count; }

    public synchronized void setCount(int count) { this.count = count; }

}

同步方法public synchronized void setCount(int count) { this.count = count; } 事实上相当于:

public void setCount(int count) {
    synchronized(this) { // 在此获得this锁
         this.count = count;

    } // 在此释放this锁
}

红色部分表示需要同步的代码段,该区域为“危险区域”,如果两个以上的线程同时执行,会引发冲突,因此,要更改SharedResource的内部状态,必须先获得SharedResource实例的锁。

退出synchronized块时,线程拥有的锁自动释放,于是,别的线程又可以获取该锁了。

为了提高性能,不一定要锁定this,例如,SharedResource有两个独立变化的变量:

public class SharedResouce {
    private int a = 0;
    private int b = 0;

    public synchronized void setA(int a) { this.a = a; }

    public synchronized void setB(int b) { this.b = b; }
}

若同步整个方法,则setA()的时候无法setB(),setB()时无法setA()。为了提高性能,可以使用不同对象的锁:

public class SharedResouce {
    private int a = 0;
    private int b = 0;
    private Object sync_a = new Object();
    private Object sync_b = new Object();

    public void setA(int a) {
        synchronized(sync_a) {
            this.a = a;
        }
    }

    public synchronized void setB(int b) {
        synchronized(sync_b) {
            this.b = b;
        }
    }
}

通常,多线程之间需要协调工作。例如,浏览器的一个显示图片的线程displayThread想要执行显示图片的任务,必须等待下载线程 downloadThread将该图片下载完毕。如果图片还没有下载完,displayThread可以暂停,当downloadThread完成了任务后,再通知displayThread“图片准备完毕,可以显示了”,这时,displayThread继续执行。

以上逻辑简单的说就是:如果条件不满足,则等待。当条件满足时,等待该条件的线程将被唤醒。在Java中,这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。例如:

synchronized(obj) {
    while(!condition) {
        obj.wait();
    }
    obj.doSomething();
}

当线程A获得了obj锁后,发现条件condition不满足,无法继续下一处理,于是线程A就wait()。

在另一线程B中,如果B更改了某些条件,使得线程A的condition条件满足了,就可以唤醒线程A:

synchronized(obj) {
    condition = true;
    obj.notify();
}

需要注意的概念是:

# 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {…} 代码段内。

# 调用obj.wait()后,线程A就释放了obj的锁,否则线程B无法获得obj锁,也就无法在synchronized(obj) {…} 代码段内唤醒A。

# 当obj.wait()方法返回后,线程A需要再次获得obj锁,才能继续执行。

# 如果A1,A2,A3都在obj.wait(),则B调用obj.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)。

# obj.notifyAll()则能全部唤醒A1,A2,A3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。

# 当B调用obj.notify/notifyAll的时候,B正持有obj锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1,A2,A3中的一个才有机会获得锁继续执行。


前面讲了wait/notify机制,Thread还有一个sleep()静态方法,它也能使线程暂停一段时间。sleep与wait的不同点是: sleep并不释放锁,并且sleep的暂停和wait暂停是不一样的。obj.wait会使线程进入obj对象的等待集合中并等待唤醒。

但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。

如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在 wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。

需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException。


GuardedSuspention模式主要思想是:

当条件不满足时,线程等待,直到条件满足时,等待该条件的线程被唤醒。

我们设计一个客户端线程和一个服务器线程,客户端线程不断发送请求给服务器线程,服务器线程不断处理请求。当请求队列为空时,服务器线程就必须等待,直到客户端发送了请求。

先定义一个请求队列:Queue

package com.crackj2ee.thread;

import java.util.*;

public class Queue {
    private List queue = new LinkedList();

    public synchronized Request getRequest() {
        while(queue.size()==0) {
            try {
                this.wait();
            }
            catch(InterruptedException ie) {
                return null;
            }
        }
        return (Request)queue.remove(0);
    }

    public synchronized void putRequest(Request request) {
        queue.add(request);
        this.notifyAll();
    }

}

蓝色部分就是服务器线程的等待条件,而客户端线程在放入了一个request后,就使服务器线程等待条件满足,于是唤醒服务器线程。

客户端线程:ClientThread

package com.crackj2ee.thread;

public class ClientThread extends Thread {
    private Queue queue;
    private String clientName;

    public ClientThread(Queue queue, String clientName) {
        this.queue = queue;
        this.clientName = clientName;
    }

    public String toString() {
        return "[ClientThread-" + clientName + "]";
    }

    public void run() {
        for(int i=0; i<100; i++) {
            Request request = new Request("" + (long)(Math.random()*10000));
            System.out.println(this + " send request: " + request);
            queue.putRequest(request);
            try {
                Thread.sleep((long)(Math.random() * 10000 + 1000));
            }
            catch(InterruptedException ie) {
            }
        }
        System.out.println(this + " shutdown.");
    }
}

服务器线程:ServerThread

package com.crackj2ee.thread;
public class ServerThread extends Thread {
    private boolean stop = false;
    private Queue queue;

    public ServerThread(Queue queue) {
        this.queue = queue;
    }

    public void shutdown() {
        stop = true;
        this.interrupt();
        try {
            this.join();
        }
        catch(InterruptedException ie) {}
    }

    public void run() {
        while(!stop) {
            Request request = queue.getRequest();
            System.out.println("[ServerThread] handle request: " + request);
            try {
                Thread.sleep(2000);
            }
            catch(InterruptedException ie) {}
        }
        System.out.println("[ServerThread] shutdown.");
    }
}

服务器线程在红色部分可能会阻塞,也就是说,Queue.getRequest是一个阻塞方法。这和java标准库的许多IO方法类似。

最后,写一个Main来启动他们:

package com.crackj2ee.thread;

public class Main {

    public static void main(String[] args) {
        Queue queue = new Queue();
        ServerThread server = new ServerThread(queue);
        server.start();
        ClientThread[] clients = new ClientThread[5];
        for(int i=0; i<clients.length; i++) {
            clients[i] = new ClientThread(queue, ""+i);
            clients[i].start();
        }
        try {
            Thread.sleep(100000);
        }
        catch(InterruptedException ie) {}
        server.shutdown();
    }
}

我们启动了5个客户端线程和一个服务器线程,运行结果如下:

[ClientThread-0] send request: Request-4984
[ServerThread] handle request: Request-4984
[ClientThread-1] send request: Request-2020
[ClientThread-2] send request: Request-8980
[ClientThread-3] send request: Request-5044
[ClientThread-4] send request: Request-548
[ClientThread-4] send request: Request-6832
[ServerThread] handle request: Request-2020
[ServerThread] handle request: Request-8980
[ServerThread] handle request: Request-5044
[ServerThread] handle request: Request-548
[ClientThread-4] send request: Request-1681
[ClientThread-0] send request: Request-7859
[ClientThread-3] send request: Request-3926
[ServerThread] handle request: Request-6832
[ClientThread-2] send request: Request-9906
……

可以观察到ServerThread处理来自不同客户端的请求。

思考

Q: 服务器线程的wait条件while(queue.size()==0)能否换成if(queue.size()==0)?

A: 在这个例子中可以,因为服务器线程只有一个。但是,如果服务器线程有多个(例如Web应用程序有多个线程处理并发请求,这非常普遍),就会造成严重问题。

Q: 能否用sleep(1000)代替wait()?

A: 绝对不可以。sleep()不会释放锁,因此sleep期间别的线程根本没有办法调用getRequest()和putRequest(),导致所有相关线程都被阻塞。

Q: (Request)queue.remove(0)可以放到synchronized() {}块外面吗?

A: 不可以。因为while()是测试queue,remove()是使用queue,两者是一个原子操作,不能放在synchronized外面。

总结

多线程设计看似简单,实际上必须非常仔细地考虑各种锁定/同步的条件,稍不小心,就可能出错。并且,当线程较少时,很可能发现不了问题,一旦问题出现又难以调试。

所幸的是,已有一些被验证过的模式可以供我们使用,我们会继续介绍一些常用的多线程设计模式。

前面谈了多线程应用程序能极大地改善用户相应。例如对于一个Web应用程序,每当一个用户请求服务器连接时,服务器就可以启动一个新线程为用户服务。

然而,创建和销毁线程本身就有一定的开销,如果频繁创建和销毁线程,CPU和内存开销就不可忽略,垃圾收集器还必须负担更多的工作。因此,线程池就是为了避免频繁创建和销毁线程。

每当服务器接受了一个新的请求后,服务器就从线程池中挑选一个等待的线程并执行请求处理。处理完毕后,线程并不结束,而是转为阻塞状态再次被放入线程池中。这样就避免了频繁创建和销毁线程。

Worker Pattern实现了类似线程池的功能。首先定义Task接口:

package com.crackj2ee.thread;
public interface Task {
    void execute();
}

线程将负责执行execute()方法。注意到任务是由子类通过实现execute()方法实现的,线程本身并不知道自己执行的任务。它只负责运行一个耗时的execute()方法。

具体任务由子类实现,我们定义了一个CalculateTask和一个TimerTask:

// CalculateTask.java
package com.crackj2ee.thread;
public class CalculateTask implements Task {
    private static int count = 0;
    private int num = count;
    public CalculateTask() {
        count++;
    }
    public void execute() {
        System.out.println("[CalculateTask " + num + "] start…");
        try {
            Thread.sleep(3000);
        }
        catch(InterruptedException ie) {}
        System.out.println("[CalculateTask " + num + "] done.");
    }
}

// TimerTask.java
package com.crackj2ee.thread;
public class TimerTask implements Task {
    private static int count = 0;
    private int num = count;
    public TimerTask() {
        count++;
    }
    public void execute() {
        System.out.println("[TimerTask " + num + "] start…");
        try {
            Thread.sleep(2000);
        }
        catch(InterruptedException ie) {}
        System.out.println("[TimerTask " + num + "] done.");
    }
}

以上任务均简单的sleep若干秒。

TaskQueue实现了一个队列,客户端可以将请求放入队列,服务器线程可以从队列中取出任务:

package com.crackj2ee.thread;
import java.util.*;
public class TaskQueue {
    private List queue = new LinkedList();
    public synchronized Task getTask() {
        while(queue.size()==0) {
            try {
                this.wait();
            }
            catch(InterruptedException ie) {
                return null;
            }
        }
        return (Task)queue.remove(0);
    }
    public synchronized void putTask(Task task) {
        queue.add(task);
        this.notifyAll();
    }
}

终于到了真正的WorkerThread,这是真正执行任务的服务器线程:

package com.crackj2ee.thread;
public class WorkerThread extends Thread {
    private static int count = 0;
    private boolean busy = false;
    private boolean stop = false;
    private TaskQueue queue;
    public WorkerThread(ThreadGroup group, TaskQueue queue) {
        super(group, "worker-" + count);
        count++;
        this.queue = queue;
    }
    public void shutdown() {
        stop = true;
        this.interrupt();
        try {
            this.join();
        }
        catch(InterruptedException ie) {}
    }
    public boolean isIdle() {
        return !busy;
    }
    public void run() {
        System.out.println(getName() + " start.");       
        while(!stop) {
            Task task = queue.getTask();
            if(task!=null) {
                busy = true;
                task.execute();
                busy = false;
            }
        }
        System.out.println(getName() + " end.");
    }
}

前面已经讲过,queue.getTask()是一个阻塞方法,服务器线程可能在此wait()一段时间。此外,WorkerThread还有一个shutdown方法,用于安全结束线程。

最后是ThreadPool,负责管理所有的服务器线程,还可以动态增加和减少线程数:

package com.crackj2ee.thread;
import java.util.*;
public class ThreadPool extends ThreadGroup {
    private List threads = new LinkedList();
    private TaskQueue queue;
    public ThreadPool(TaskQueue queue) {
        super("Thread-Pool");
        this.queue = queue;
    }
    public synchronized void addWorkerThread() {
        Thread t = new WorkerThread(this, queue);
        threads.add(t);
        t.start();
    }
    public synchronized void removeWorkerThread() {
        if(threads.size()>0) {
            WorkerThread t = (WorkerThread)threads.remove(0);
            t.shutdown();
        }
    }
    public synchronized void currentStatus() {
        System.out.println("———————————————–");
        System.out.println("Thread count = " + threads.size());
        Iterator it = threads.iterator();
        while(it.hasNext()) {
            WorkerThread t = (WorkerThread)it.next();
            System.out.println(t.getName() + ": " + (t.isIdle() ? "idle" : "busy"));
        }
        System.out.println("———————————————–");
    }
}

currentStatus()方法是为了方便调试,打印出所有线程的当前状态。

最后,Main负责完成main()方法:

package com.crackj2ee.thread;
public class Main {
    public static void main(String[] args) {
        TaskQueue queue = new TaskQueue();
        ThreadPool pool = new ThreadPool(queue);
        for(int i=0; i<10; i++) {
            queue.putTask(new CalculateTask());
            queue.putTask(new TimerTask());
        }
        pool.addWorkerThread();
        pool.addWorkerThread();
        doSleep(8000);
        pool.currentStatus();
        pool.addWorkerThread();
        pool.addWorkerThread();
        pool.addWorkerThread();
        pool.addWorkerThread();
        pool.addWorkerThread();
        doSleep(5000);
        pool.currentStatus();
    }
    private static void doSleep(long ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}

main()一开始放入了20个Task,然后动态添加了一些服务线程,并定期打印线程状态,运行结果如下:

worker-0 start.
[CalculateTask 0] start…
worker-1 start.
[TimerTask 0] start…
[TimerTask 0] done.
[CalculateTask 1] start…
[CalculateTask 0] done.
[TimerTask 1] start…
[CalculateTask 1] done.
[CalculateTask 2] start…
[TimerTask 1] done.
[TimerTask 2] start…
[TimerTask 2] done.
[CalculateTask 3] start…
———————————————–
Thread count = 2
worker-0: busy
worker-1: busy
———————————————–
[CalculateTask 2] done.
[TimerTask 3] start…
worker-2 start.
[CalculateTask 4] start…
worker-3 start.
[TimerTask 4] start…
worker-4 start.
[CalculateTask 5] start…
worker-5 start.
[TimerTask 5] start…
worker-6 start.
[CalculateTask 6] start…
[CalculateTask 3] done.
[TimerTask 6] start…
[TimerTask 3] done.
[CalculateTask 7] start…
[TimerTask 4] done.
[TimerTask 7] start…
[TimerTask 5] done.
[CalculateTask 8] start…
[CalculateTask 4] done.
[TimerTask 8] start…
[CalculateTask 5] done.
[CalculateTask 9] start…
[CalculateTask 6] done.
[TimerTask 9] start…
[TimerTask 6] done.
[TimerTask 7] done.
———————————————–
Thread count = 7
worker-0: idle
worker-1: busy
worker-2: busy
worker-3: idle
worker-4: busy
worker-5: busy
worker-6: busy
———————————————–
[CalculateTask 7] done.
[CalculateTask 8] done.
[TimerTask 8] done.
[TimerTask 9] done.
[CalculateTask 9] done.

仔细观察:一开始只有两个服务器线程,因此线程状态都是忙,后来线程数增多,6个线程中的两个状态变成idle,说明处于wait()状态。

思考:本例的线程调度算法其实根本没有,因为这个应用是围绕TaskQueue设计的,不是以Thread Pool为中心设计的。因此,Task调度取决于TaskQueue的getTask()方法,你可以改进这个方法,例如使用优先队列,使优先级高的任务先被执行。

如果所有的服务器线程都处于busy状态,则说明任务繁忙,TaskQueue的队列越来越长,最终会导致服务器内存耗尽。因此,可以限制 TaskQueue的等待任务数,超过最大长度就拒绝处理。许多Web服务器在用户请求繁忙时就会拒绝用户:HTTP 503 SERVICE UNAVAILABLE

多线程读写同一个对象的数据是很普遍的,通常,要避免读写冲突,必须保证任何时候仅有一个线程在写入,有线程正在读取的时候,写入操作就必须等待。简单说,就是要避免“写-写”冲突和“读-写”冲突。但是同时读是允许的,因为“读-读”不冲突,而且很安全。

要实现以上的ReadWriteLock,简单的使用synchronized就不行,我们必须自己设计一个ReadWriteLock类,在读之前,必须先获得“读锁”,写之前,必须先获得“写锁”。举例说明:

DataHandler对象保存了一个可读写的char[]数组:

package com.crackj2ee.thread;

public class DataHandler {
    // store data:
    private char[] buffer = "AAAAAAAAAA".toCharArray();

    private char[] doRead() {
        char[] ret = new char[buffer.length];
        for(int i=0; i<buffer.length; i++) {
            ret[i] = buffer[i];
            sleep(3);
        }
        return ret;
    }

    private void doWrite(char[] data) {
        if(data!=null) {
            buffer = new char[data.length];
            for(int i=0; i<buffer.length; i++) {
                buffer[i] = data[i];
                sleep(10);
            }
        }
    }

    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}

doRead()和doWrite()方法是非线程安全的读写方法。为了演示,加入了sleep(),并设置读的速度大约是写的3倍,这符合通常的情况。

为了让多线程能安全读写,我们设计了一个ReadWriteLock:

package com.crackj2ee.thread;
public class ReadWriteLock {
    private int readingThreads = 0;
    private int writingThreads = 0;
    private int waitingThreads = 0; // waiting for write
    private boolean preferWrite = true;

    public synchronized void readLock() throws InterruptedException {
        while(writingThreads>0 || (preferWrite && waitingThreads>0))
            this.wait();
        readingThreads++;
    }

    public synchronized void readUnlock() {
        readingThreads–;
        preferWrite = true;
        notifyAll();
    }

    public synchronized void writeLock() throws InterruptedException {
        waitingThreads++;
        try {
            while(readingThreads>0 || writingThreads>0)
                this.wait();
        }
        finally {
            waitingThreads–;
        }
        writingThreads++;
    }

    public synchronized void writeUnlock() {
        writingThreads–;
        preferWrite = false;
        notifyAll();
    }
}

readLock()用于获得读锁,readUnlock()释放读锁,writeLock()和writeUnlock()一样。由于锁用完必须释放,因此,必须保证lock和unlock匹配。我们修改DataHandler,加入ReadWriteLock:

package com.crackj2ee.thread;
public class DataHandler {
    // store data:
    private char[] buffer = "AAAAAAAAAA".toCharArray();
    // lock:
    private ReadWriteLock lock = new ReadWriteLock();

    public char[] read(String name) throws InterruptedException {
        System.out.println(name + " waiting for read…");
        lock.readLock();
        try {
            char[] data = doRead();
            System.out.println(name + " reads data: " + new String(data));
            return data;
        }
        finally {
            lock.readUnlock();
        }
    }

    public void write(String name, char[] data) throws InterruptedException {
        System.out.println(name + " waiting for write…");
        lock.writeLock();
        try {
            System.out.println(name + " wrote data: " + new String(data));
            doWrite(data);
        }
        finally {
            lock.writeUnlock();
        }
    }

    private char[] doRead() {
        char[] ret = new char[buffer.length];
        for(int i=0; i<buffer.length; i++) {
            ret[i] = buffer[i];
            sleep(3);
        }
        return ret;
    }
    private void doWrite(char[] data) {
        if(data!=null) {
            buffer = new char[data.length];
            for(int i=0; i<buffer.length; i++) {
                buffer[i] = data[i];
                sleep(10);
            }
        }
    }
    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}

public方法read()和write()完全封装了底层的ReadWriteLock,因此,多线程可以安全地调用这两个方法:

// ReadingThread不断读取数据:
package com.crackj2ee.thread;
public class ReadingThread extends Thread {
    private DataHandler handler;
    public ReadingThread(DataHandler handler) {
        this.handler = handler;
    }
    public void run() {
        for(;;) {
            try {
                char[] data = handler.read(getName());
                Thread.sleep((long)(Math.random()*1000+100));
            }
            catch(InterruptedException ie) {
                break;
            }
        }
    }
}

// WritingThread不断写入数据,每次写入的都是10个相同的字符:
package com.crackj2ee.thread;
public class WritingThread extends Thread {
    private DataHandler handler;
    public WritingThread(DataHandler handler) {
        this.handler = handler;
    }
    public void run() {
        char[] data = new char[10];
        for(;;) {
            try {
                fill(data);
                handler.write(getName(), data);
                Thread.sleep((long)(Math.random()*1000+100));
            }
            catch(InterruptedException ie) {
                break;
            }
        }
    }
    // 产生一个A-Z随机字符,填入char[10]:
    private void fill(char[] data) {
        char c = (char)(Math.random()*26+’A');
        for(int i=0; i<data.length; i++)
            data[i] = c;
    }
}

最后Main负责启动这些线程:

package com.crackj2ee.thread;
public class Main {
    public static void main(String[] args) {
        DataHandler handler = new DataHandler();
        Thread[] ts = new Thread[] {
                new ReadingThread(handler),
                new ReadingThread(handler),
                new ReadingThread(handler),
                new ReadingThread(handler),
                new ReadingThread(handler),
                new WritingThread(handler),
                new WritingThread(handler)
        };
        for(int i=0; i<ts.length; i++) {
            ts[i].start();
        }
    }
}

我们启动了5个读线程和2个写线程,运行结果如下:

Thread-0 waiting for read…
Thread-1 waiting for read…
Thread-2 waiting for read…
Thread-3 waiting for read…
Thread-4 waiting for read…
Thread-5 waiting for write…
Thread-6 waiting for write…
Thread-4 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-0 reads data: AAAAAAAAAA
Thread-5 wrote data: EEEEEEEEEE
Thread-6 wrote data: MMMMMMMMMM
Thread-1 waiting for read…
Thread-4 waiting for read…
Thread-1 reads data: MMMMMMMMMM
Thread-4 reads data: MMMMMMMMMM
Thread-2 waiting for read…
Thread-2 reads data: MMMMMMMMMM
Thread-0 waiting for read…
Thread-0 reads data: MMMMMMMMMM
Thread-4 waiting for read…
Thread-4 reads data: MMMMMMMMMM
Thread-2 waiting for read…
Thread-5 waiting for write…
Thread-2 reads data: MMMMMMMMMM
Thread-5 wrote data: GGGGGGGGGG
Thread-6 waiting for write…
Thread-6 wrote data: AAAAAAAAAA
Thread-3 waiting for read…
Thread-3 reads data: AAAAAAAAAA
……

可以看到,每次读/写都是完整的原子操作,因为我们每次写入的都是10个相同字符。并且,每次读出的都是最近一次写入的内容。

如果去掉ReadWriteLock:

package com.crackj2ee.thread;
public class DataHandler {

    // store data:
    private char[] buffer = "AAAAAAAAAA".toCharArray();

    public char[] read(String name) throws InterruptedException {
        char[] data = doRead();
        System.out.println(name + " reads data: " + new String(data));
        return data;
    }
    public void write(String name, char[] data) throws InterruptedException {
        System.out.println(name + " wrote data: " + new String(data));
        doWrite(data);
    }

    private char[] doRead() {
        char[] ret = new char[10];
        for(int i=0; i<10; i++) {
            ret[i] = buffer[i];
            sleep(3);
        }
        return ret;
    }
    private void doWrite(char[] data) {
        for(int i=0; i<10; i++) {
            buffer[i] = data[i];
            sleep(10);
        }
    }
    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}

运行结果如下:

Thread-5 wrote data: AAAAAAAAAA
Thread-6 wrote data: MMMMMMMMMM
Thread-0 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-4 reads data: AAAAAAAAAA
Thread-2 reads data: MAAAAAAAAA
Thread-3 reads data: MAAAAAAAAA
Thread-5 wrote data: CCCCCCCCCC
Thread-1 reads data: MAAAAAAAAA
Thread-0 reads data: MAAAAAAAAA
Thread-4 reads data: MAAAAAAAAA
Thread-6 wrote data: EEEEEEEEEE
Thread-3 reads data: EEEEECCCCC
Thread-4 reads data: EEEEEEEEEC
Thread-1 reads data: EEEEEEEEEE

可以看到在Thread-6写入EEEEEEEEEE的过程中,3个线程读取的内容是不同的。

思考

java的synchronized提供了最底层的物理锁,要在synchronized的基础上,实现自己的逻辑锁,就必须仔细设计ReadWriteLock。

Q: lock.readLock()为什么不放入try{ } 内?
A: 因为readLock()会抛出InterruptedException,导致readingThreads++不执行,而readUnlock()在 finally{ } 中,导致readingThreads–执行,从而使readingThread状态出错。writeLock()也是类似的。

Q: preferWrite有用吗?
A: 如果去掉preferWrite,线程安全不受影响。但是,如果读取线程很多,上一个线程还没有读取完,下一个线程又开始读了,就导致写入线程长时间无法获得writeLock;如果写入线程等待的很多,一个接一个写,也会导致读取线程长时间无法获得readLock。preferWrite的作用是让读 /写交替执行,避免由于读线程繁忙导致写无法进行和由于写线程繁忙导致读无法进行。

Q: notifyAll()换成notify()行不行?
A: 不可以。由于preferWrite的存在,如果一个线程刚读取完毕,此时preferWrite=true,再notify(),若恰好唤醒的是一个读线程,则while(writingThreads>0 || (preferWrite && waitingThreads>0))可能为true导致该读线程继续等待,而等待写入的线程也处于wait()中,结果所有线程都处于wait ()状态,谁也无法唤醒谁。因此,notifyAll()比notify()要来得安全。程序验证notify()带来的死锁:

Thread-0 waiting for read…
Thread-1 waiting for read…
Thread-2 waiting for read…
Thread-3 waiting for read…
Thread-4 waiting for read…
Thread-5 waiting for write…
Thread-6 waiting for write…
Thread-0 reads data: AAAAAAAAAA
Thread-4 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-5 wrote data: CCCCCCCCCC
Thread-2 waiting for read…
Thread-1 waiting for read…
Thread-3 waiting for read…
Thread-0 waiting for read…
Thread-4 waiting for read…
Thread-6 wrote data: LLLLLLLLLL
Thread-5 waiting for write…
Thread-6 waiting for write…
Thread-2 reads data: LLLLLLLLLL
Thread-2 waiting for read…
(运行到此不动了)

注意到这种死锁是由于所有线程都在等待别的线程唤醒自己,结果都无法醒过来。这和两个线程希望获得对方已有的锁造成死锁不同。因此多线程设计的难度远远高于单线程应用。

2005年03月31日

 jad文件的错误代码,分享

jad ( Java Application Discriptor ) 文件是J2ME的一个重要的组成部分,在我们发布J2ME程序的时候,jad文件经常报出各种错误,如:

      com.sun.kvem.midletsuite.InvalidJadException: Reason = 13

其中的错误原因是1-52的代码,很让人费解。我在网上找到了这些代码的解释,和大家分享如下:

public static final int JAD_SERVER_NOT_FOUND = 1;
public static final int JAD_NOT_FOUND = 2;
public static final int MISSING_PROVIDER_CERT = 4;
public static final int CORRUPT_PROVIDER_CERT = 5;
public static final int UNKNOWN_CA = 6;
public static final int INVALID_PROVIDER_CERT = 7;
public static final int CORRUPT_SIGNATURE = 8;
public static final int INVALID_SIGNATURE = 9;
public static final int UNSUPPORTED_CERT = 10;
public static final int EXPIRED_PROVIDER_CERT = 11;
public static final int EXPIRED_CA_KEY = 12;
public static final int MISSING_SUITE_NAME = 13;
public static final int MISSING_VENDOR = 14;
public static final int MISSING_VERSION = 15;
public static final int INVALID_VERSION = 16;
public static final int OLD_VERSION = 17;
public static final int MISSING_JAR_URL = 18;
public static final int JAR_SERVER_NOT_FOUND = 19;
public static final int JAR_NOT_FOUND = 20;
public static final int MISSING_JAR_SIZE = 21;
public static final int SUITE_NAME_MISMATCH = 25;
public static final int VERSION_MISMATCH = 26;
public static final int VENDOR_MISMATCH = 27;
public static final int INVALID_KEY = 28;
public static final int INVALID_VALUE = 29;
public static final int INSUFFICIENT_STORAGE = 30;
public static final int JAR_SIZE_MISMATCH = 31;
public static final int NEW_VERSION = 32;
public static final int UNAUTHORIZED = 33;
public static final int JAD_MOVED = 34;
public static final int CANNOT_AUTH = 35;
public static final int CORRUPT_JAR = 36;
public static final int INVALID_JAD_TYPE = 37;
public static final int INVALID_JAR_TYPE = 38;
public static final int ALREADY_INSTALLED = 39;
public static final int DEVICE_INCOMPATIBLE = 40;
public static final int MISSING_CONFIGURATION = 41;
public static final int MISSING_PROFILE = 42;
public static final int INVALID_JAD_URL = 43;
public static final int INVALID_JAR_URL = 44;
public static final int PUSH_DUP_FAILURE = 45;
public static final int PUSH_FORMAT_FAILURE = 46;
public static final int PUSH_PROTO_FAILURE = 47;
public static final int PUSH_CLASS_FAILURE = 48;
public static final int AUTHORIZATION_FAILURE = 49;
public static final int ATTRIBUTE_MISMATCH = 50;
public static final int PROXY_AUTH = 51;
public static final int TRUSTED_OVERWRITE_FAILURE = 52;

有了这些代码的解释,我们就很容易知道错误的原因了。

     另外,大家知道,有的手机在安装jar文件的时候,不需要jad文件。这是因为手机厂商在操作系统中内置了对jar文件的解析功能。就如同手机自动生成jad文件,然后使用这个jad文件安装jar文件一样。

2005年03月26日

NokiaS40S60开发平台1.0已知问题(翻译)

作者:陈跃峰

出自:http://blog.csdn.net/mailbomb

 

1、  Nokia3300不支MMA(声音处理)类库。

2、  Image.getGraphics()方法在不同的软件版本中工作不同,该方法无法在新版本的76503650N-Gage中正常工作。即这些机器中无法实现双缓冲技术。

3、  Nokia76503650N-Gage,无法控制背景灯和震动。

4、  同时播放声音在S60模拟器上可以运行,但是真机不支持。

5、  7650不支持WMA(短信息API)

6、  7210SDK1.0softkey1softkey2也产生leftright按键代码。

7、  S60中堆内存不同。一般S60设备有2M,而3600只有1.6M

8、  记录集枚举问题。在S40SDK和真机中,使用RecordStore.deleteRecord(id)以后,枚举工作不正常。

9、  Nokia6230 MIDP Concept SDK beta0.2不支持CLDC1.1

10、              NUL字符(0×00)TextBox中有问题。S401.0 SDK和真机中都存在该问题。

11、              如果MIDlet在后台运行时,MIDI声音不停止。S601.0中存在该问题。

12、              当正在运行的MIDlet被系统Screen隐含调用的类选择显示屏幕时有问题。在S60 1.0中存在该问题。

13、              S40界面风格中的字体大小不一致。在S40 1.0SDK和真机,以及7250i310061086200中。

14、              Item.getLable()方法在ChoiceGroup中无法返回正确的label

15、              Nokia MIDP 1.0中,DeviceControl.setLights(0,0)没有关闭键区灯。屏幕等关闭,而键区等依然亮着。涉及具有翻页键盘的S40 1.0设备。

16、              S60 1.0设备中,Date对象无法返回当前时间。

 

详细文档参看:

       http://www.forum.nokia.com/ndsCookieBuilder?fileParamID=3895

2005年03月17日

SUN J2ME 技术区:  http://java.sun.com/j2me/ 

中国JAVA 手机网:http://www.cnjm.net/

NOKIA 论坛 : http://www.forum.nokia.com

移动开发者论坛: http://mobisoft.cn/bbs/

中文移动开发者博客:http://mobisoft.cn/blog/

嵌入开发网: http://www.embed.com.cn

J2ME POLISH:http://www.j2mepolish.org

火狐下载中心:  http://down.skyhu.com/

J2ME 开发网:  http://www.j2medev.com/

A huge repository of MIDlets and the chance to make your game publicly available:    http://midlet.org

Nice resource with mixed info on J2mE :   http://www.billday.com/j2me/

Mobile game review site, see what games are around and how they rate:  http://www.midlet-review.com

Excellent collection of commercial games :  http://games.macrospace.com

Agood resource for J2ME related news, tutorials and articles :   http://www.microjava.com

IGN’s wireless gaming section :   http://wireless.ign.com

All the info you will need to get started with Brew :   http://www.qualcomm.com/brew

List of devices and device specs :  http://www.kobjects.org/devicedb

Another detailed list of java devices :  http://wireless.java.sun.com/device/

Easy to use publicly available fixed point library for J2ME :  

     http://home.rochester.rr.com/ohommes/MathFP/

Motorola’s developer site: http://www.motocoder.com

The biggest mophun resource around :  http://www.mophun.com