嵌入式目标板程序的压缩(1)--学习使用LZMA SDK
2015-02-12 11:10
288 查看
嵌入式目标板程序的压缩(1)--学习使用LZMA SDK
点击打开链接
之前完成了串口升级到功能之后,觉得700多K一分多钟的传输时间应该还有优化空间。波特率最大就115200bps,而且有些特殊时候还要降到19200用,所以减少程序大小是个途径。一般要显著减少代码量比较困难,除非程序里面“废话”太多;而且做编译器优化实验观察,差异还不到10%。压缩自然是个好方法,某次打rar包仔细看了下,500多K的程序压到不到200k,看来效果比较明显。于是有了以下想法:
一。找一个公开的压缩算法,最好有良好的C接口
二。测试该算法的压缩性能,压缩比大概跟Winrar差不多,不多于1倍就好
三。移植解压部分到目标板程序,实现[上位机压缩程序]->[串口传输]->[目标板解压升级程序]的串口升级流程
首先是找一个公开的压缩算法:
LZW
http://zh.wikipedia.org/zh/LZW
LZMA
http://zh.wikipedia.org/wiki/LZMA
.....
然后不停地搜源码下源码,下了一堆。浏览了一下”战利品“,最后发现有个LZMA SDK的C实现比较合我胃口。用vs写了个console工程,就调用LzmaLib里面的LzmaCompress和LzmaUncompress接口,参数全部默认,实现了一个简单的命令行压缩工具。
这里小结下Lzma SDK的使用:
最顶层的接口是LzmaCompress和LzmaUncompress,位于LzmaLib.c中,声明如下:
//--------------------------------------------//
MY_STDAPI LzmaCompress(unsigned char *dest, size_t *destLen,
const unsigned char *src, size_t srcLen,
unsigned char *outProps, size_t *outPropsSize, /* *outPropsSize must be = 5 */
int level, /* 0 <= level <= 9, default = 5 */
unsigned dictSize, /* default = (1 << 24) */
int lc, /* 0 <= lc <= 8, default = 3 */
int lp, /* 0 <= lp <= 4, default = 0 */
int pb, /* 0 <= pb <= 4, default = 2 */
int fb, /* 5 <= fb <= 273, default = 32 */
int numThreads /* 1 or 2, default = 2 */
);
//--------------------------------------------//
MY_STDAPI LzmaUncompress(unsigned char *dest, size_t *destLen,
const unsigned char *src, SizeT *srcLen,
const unsigned char *props, size_t propsSize);
//--------------------------------------------//
调用很简单,压缩接口只要填入进出缓冲区和默认参数就行了,注意到是outProps参数是输出参数,这个数组值在相应解压的时候用;解压接口参数更少,注意要填好对应的outProps参数,还有提供足够大destLen的缓冲区。看代码可以发现,outProps数组其实就是压缩参数lc,lp,bp和dictSize算出来的。
写好便开始测试,首先看看压缩的效果:把目标板的程序试着压缩,576K压缩到153K。不错!满足了我的预期。
其次试试压缩后解压是否正确,是否生成的与原来文件一模一样;换几组参数再测,没问题。
然后分别找来几个不同类型不同大小的文件,例如txt exe pdf doc bmp rar zip,压缩结果和winRAR相当。还查到程序的一些bug,不过对于上百兆的文件解压结果有误,一时没查出原因,只好先搁置。
最后换了几组参数测,打算把从level到pb五个参数所有排列的参数都测一遍。不想改程序,很自然就求助于批处理/脚本了。(待续)
为压缩函数,LzmaUncompress 为解压缩函数。
MY_STDAPI LzmaCompress(unsigned char *dest, size_t *destLen, const unsigned char *src, size_t srcLen,
unsigned char *outProps, size_t *outPropsSize,
int level,
unsigned dictSize,
int lc,
int lp,
int pb,
int fb,
int numThreads
);
MY_STDAPI LzmaUncompress(unsigned char *dest, size_t *destLen, const unsigned char *src, SizeT *srcLen,
const unsigned char *props, size_t propsSize);
导入Types.h 和 Lzmalib.h 到工程中。
代码如下:
#include "stdafx.h"
#include "LzmaLib.h"
#pragma comment(lib,"lzma.lib")
int _tmain(int argc, _TCHAR* argv[])
{
FILE* pFile = _tfopen(_T("file.dat"), _T("rb"));
if (pFile == NULL)
{
_ftprintf(stderr, _T("Error to Open the file!"));
return - 1;
}
fseek(pFile, 0, SEEK_END);
size_t srcLen = ftell(pFile);
rewind(pFile);
size_t destLen = srcLen * 2;
unsigned char* psrcRead = new unsigned char[srcLen]; //原始文件数据
unsigned char* pDecomress = new unsigned char[srcLen]; //存放解压缩数据
unsigned char* pLzma = new unsigned char[destLen]; //存放压缩数据
fread(psrcRead, sizeof(char), srcLen, pFile);
unsigned char prop[5] =
{
0
};
size_t sizeProp = 5;
if (SZ_OK != LzmaCompress(pLzma, &destLen, psrcRead, srcLen, prop,
&sizeProp, 9, (1 << 24), 3, 0, 2, 32, 2))
{
//出错了
_ftprintf(stderr, _T("压缩时出错!"));
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pFile);
return - 1;
}
FILE* pCompressFile = _tfopen(_T("compress.dat"), _T("wb"));
//写入压缩后的数据
if (pCompressFile == NULL)
{
_ftprintf(stderr, _T("创建文件出错!"));
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pFile);
return - 1;
}
fwrite(pLzma, sizeof(char), destLen, pCompressFile);
fclose(pCompressFile);
FILE* pDecompressFile = _tfopen(_T("decompress.dat"), _T("wb"));
//写入解压缩数据
if (pDecompressFile == NULL)
{
_ftprintf(stderr, _T("写入数据出错!"));
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pFile);
return - 1;
}
//注意:解压缩时props参数要使用压缩时生成的outProps,这样才能正常解压缩
if (SZ_OK != LzmaUncompress(pDecomress, &srcLen, pLzma, &destLen, prop, 5))
{
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pDecompressFile);
fclose(pFile);
return - 1;
}
fwrite(pDecomress, sizeof(char), srcLen, pDecompressFile);
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pDecompressFile);
fclose(pFile);
return 0;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#include "LzmaLib.h"
/*_7ZIP_ST must be defined*/
#define BUF_SZ 8192
int main(int argc, char* argv[])
{
size_t srcLen = BUF_SZ;
size_t destLen = BUF_SZ * 2;
unsigned char psrcRead[BUF_SZ*2]; //原始文件数据
unsigned char pDecomress[BUF_SZ*2]; //存放解压缩数据
unsigned char pLzma[BUF_SZ*2]; //存放压缩数据
unsigned char prop[5] =
{
0
};
size_t sizeProp = 5;
int idx;
memset(psrcRead, 0, BUF_SZ);
for(idx = 0; idx < 8192/10; idx++)
{
psrcRead[0 + idx*10] = 0x00;
psrcRead[1 + idx*10] = 0x11+idx;
psrcRead[2 + idx*10] = 0x22+idx;
psrcRead[3 + idx*10] = 0x33;
psrcRead[4 + idx*10] = 0x23;
psrcRead[5 + idx*10] = 0x25+idx;
psrcRead[6 + idx*10] = 0x01;
psrcRead[7 + idx*10] = 0x11;
psrcRead[8 + idx*10] = 0x01;
psrcRead[9 + idx*10] = 0x01;
}
if (SZ_OK != LzmaCompress(pLzma, &destLen, psrcRead, srcLen, prop,
&sizeProp, 9, (1 << 12), 3, 0, 2, 32, 2))
{
//出错了
return - 1;
}
memset(pDecomress, 0, BUF_SZ*2);
//注意:解压缩时props参数要使用压缩时生成的outProps,这样才能正常解压缩
if (SZ_OK != LzmaUncompress(pDecomress, &srcLen, pLzma, &destLen, prop, 5))
{
return - 1;
}
if(0 == memcmp(pDecomress, psrcRead, BUF_SZ))
{
printf("compress and uncompress succeeds.\r\n");
}
return 0;
}
zlib使用:
zlib 是通用的压缩库,提供了一套 in-memory 压缩和解压函数,并能检测解压出来的数据的完整性(integrity)。下面介绍两个最有用的函数——compress 和 uncompress。
int compress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen);
compress函数将 source 缓冲区中的内容压缩到 dest 缓冲区。 sourceLen 表示source 缓冲区的大小(以字节计)。注意函数的第二个参数 destLen 是传址调用。当调用函数时,destLen表示 dest 缓冲区的大小,destLen > (sourceLen + 12)*100.1%。当函数退出后,destLen 表示压缩后缓冲区的实际大小。此时 destLen / sourceLen 正好是压缩率。
compress 若成功,则返回 Z_OK;若没有足够内存,则返回 Z_MEM_ERROR;若输出缓冲区不够大,则返回 Z_BUF_ERROR。
int uncompress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen);
uncompress 函数将 source 缓冲区的内容解压缩到 dest 缓冲区。sourceLen 是 source 缓冲区的大小(以字节计)。注意函数的第二个参数 destLen 是传址调用。当调用函数时,destLen 表示 dest 缓冲区的大小, dest 缓冲区要足以容下解压后的数据。在进行解压缩时,需要提前知道被压缩的数据解压出来会有多大。这就要求在进行压缩之前,保存原始数据的大小(也就是解压后的数据的大小)。这不是 zlib 函数库的功能,需要我们做额外的工作。当函数退出后,
destLen
是解压出来的数据的实际大小。
uncompress 若成功,则返回 Z_OK ;若没有足够内存,则返回 Z_MEM_ERROR;若输出缓冲区不够大,则返回 Z_BUF_ERROR。若输入数据有误,则返回 Z_DATA_ERROR。
代码如下:
#include "stdafx.h"
#include <cstring>
#include <cstdlib>
#include <iostream>
#include "zlib.h"
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
int err = 0;
Byte compr[200] = {0}, uncompr[200] = {0}; // big enough
uLong comprLen = 0, uncomprLen = 0;
const char* hello = "12345678901234567890123456789012345678901234567890";
uLong len = strlen(hello) + 1;
comprLen = sizeof(compr) / sizeof(compr[0]);
err = compress(compr, &comprLen, (const Bytef*)hello, len);
if (err != Z_OK)
{
cerr << "compess error: " << err << '\n';
exit(1);
}
cout << "orignal size: " << len
<< " , compressed size : " << comprLen << '\n';
strcpy((char*)uncompr, "garbage");
err = uncompress(uncompr, &uncomprLen, compr, comprLen);
if (err != Z_OK)
{
cerr << "uncompess error: " << err << '\n';
exit(1);
}
cout << "orignal size: " << len
<< " , uncompressed size : " << uncomprLen << '\n';
if (strcmp((char*)uncompr, hello))
{
cerr << "BAD uncompress!!!\n";
exit(1);
}
else
{
cout << "uncompress() succeed: \n" << (char*)uncompr;
}
}
点击打开链接
之前完成了串口升级到功能之后,觉得700多K一分多钟的传输时间应该还有优化空间。波特率最大就115200bps,而且有些特殊时候还要降到19200用,所以减少程序大小是个途径。一般要显著减少代码量比较困难,除非程序里面“废话”太多;而且做编译器优化实验观察,差异还不到10%。压缩自然是个好方法,某次打rar包仔细看了下,500多K的程序压到不到200k,看来效果比较明显。于是有了以下想法:
一。找一个公开的压缩算法,最好有良好的C接口
二。测试该算法的压缩性能,压缩比大概跟Winrar差不多,不多于1倍就好
三。移植解压部分到目标板程序,实现[上位机压缩程序]->[串口传输]->[目标板解压升级程序]的串口升级流程
首先是找一个公开的压缩算法:
LZW
http://zh.wikipedia.org/zh/LZW
LZMA
http://zh.wikipedia.org/wiki/LZMA
.....
然后不停地搜源码下源码,下了一堆。浏览了一下”战利品“,最后发现有个LZMA SDK的C实现比较合我胃口。用vs写了个console工程,就调用LzmaLib里面的LzmaCompress和LzmaUncompress接口,参数全部默认,实现了一个简单的命令行压缩工具。
这里小结下Lzma SDK的使用:
最顶层的接口是LzmaCompress和LzmaUncompress,位于LzmaLib.c中,声明如下:
//--------------------------------------------//
MY_STDAPI LzmaCompress(unsigned char *dest, size_t *destLen,
const unsigned char *src, size_t srcLen,
unsigned char *outProps, size_t *outPropsSize, /* *outPropsSize must be = 5 */
int level, /* 0 <= level <= 9, default = 5 */
unsigned dictSize, /* default = (1 << 24) */
int lc, /* 0 <= lc <= 8, default = 3 */
int lp, /* 0 <= lp <= 4, default = 0 */
int pb, /* 0 <= pb <= 4, default = 2 */
int fb, /* 5 <= fb <= 273, default = 32 */
int numThreads /* 1 or 2, default = 2 */
);
//--------------------------------------------//
MY_STDAPI LzmaUncompress(unsigned char *dest, size_t *destLen,
const unsigned char *src, SizeT *srcLen,
const unsigned char *props, size_t propsSize);
//--------------------------------------------//
调用很简单,压缩接口只要填入进出缓冲区和默认参数就行了,注意到是outProps参数是输出参数,这个数组值在相应解压的时候用;解压接口参数更少,注意要填好对应的outProps参数,还有提供足够大destLen的缓冲区。看代码可以发现,outProps数组其实就是压缩参数lc,lp,bp和dictSize算出来的。
写好便开始测试,首先看看压缩的效果:把目标板的程序试着压缩,576K压缩到153K。不错!满足了我的预期。
其次试试压缩后解压是否正确,是否生成的与原来文件一模一样;换几组参数再测,没问题。
然后分别找来几个不同类型不同大小的文件,例如txt exe pdf doc bmp rar zip,压缩结果和winRAR相当。还查到程序的一些bug,不过对于上百兆的文件解压结果有误,一时没查出原因,只好先搁置。
最后换了几组参数测,打算把从level到pb五个参数所有排列的参数都测一遍。不想改程序,很自然就求助于批处理/脚本了。(待续)
7zip 压缩(LzmaCompress)解压缩 (LzmaUncompress) 函数使用
在C目录中有算法文件,进入Util\LzmaLib目录,编译生成LIB库,导出了以下两函数,LzmaCompress为压缩函数,LzmaUncompress 为解压缩函数。
MY_STDAPI LzmaCompress(unsigned char *dest, size_t *destLen, const unsigned char *src, size_t srcLen,
unsigned char *outProps, size_t *outPropsSize,
int level,
unsigned dictSize,
int lc,
int lp,
int pb,
int fb,
int numThreads
);
MY_STDAPI LzmaUncompress(unsigned char *dest, size_t *destLen, const unsigned char *src, SizeT *srcLen,
const unsigned char *props, size_t propsSize);
导入Types.h 和 Lzmalib.h 到工程中。
代码如下:
#include "stdafx.h"
#include "LzmaLib.h"
#pragma comment(lib,"lzma.lib")
int _tmain(int argc, _TCHAR* argv[])
{
FILE* pFile = _tfopen(_T("file.dat"), _T("rb"));
if (pFile == NULL)
{
_ftprintf(stderr, _T("Error to Open the file!"));
return - 1;
}
fseek(pFile, 0, SEEK_END);
size_t srcLen = ftell(pFile);
rewind(pFile);
size_t destLen = srcLen * 2;
unsigned char* psrcRead = new unsigned char[srcLen]; //原始文件数据
unsigned char* pDecomress = new unsigned char[srcLen]; //存放解压缩数据
unsigned char* pLzma = new unsigned char[destLen]; //存放压缩数据
fread(psrcRead, sizeof(char), srcLen, pFile);
unsigned char prop[5] =
{
0
};
size_t sizeProp = 5;
if (SZ_OK != LzmaCompress(pLzma, &destLen, psrcRead, srcLen, prop,
&sizeProp, 9, (1 << 24), 3, 0, 2, 32, 2))
{
//出错了
_ftprintf(stderr, _T("压缩时出错!"));
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pFile);
return - 1;
}
FILE* pCompressFile = _tfopen(_T("compress.dat"), _T("wb"));
//写入压缩后的数据
if (pCompressFile == NULL)
{
_ftprintf(stderr, _T("创建文件出错!"));
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pFile);
return - 1;
}
fwrite(pLzma, sizeof(char), destLen, pCompressFile);
fclose(pCompressFile);
FILE* pDecompressFile = _tfopen(_T("decompress.dat"), _T("wb"));
//写入解压缩数据
if (pDecompressFile == NULL)
{
_ftprintf(stderr, _T("写入数据出错!"));
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pFile);
return - 1;
}
//注意:解压缩时props参数要使用压缩时生成的outProps,这样才能正常解压缩
if (SZ_OK != LzmaUncompress(pDecomress, &srcLen, pLzma, &destLen, prop, 5))
{
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pDecompressFile);
fclose(pFile);
return - 1;
}
fwrite(pDecomress, sizeof(char), srcLen, pDecompressFile);
delete [] psrcRead;
delete [] pDecomress;
delete [] pLzma;
fclose(pDecompressFile);
fclose(pFile);
return 0;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#include "LzmaLib.h"
/*_7ZIP_ST must be defined*/
#define BUF_SZ 8192
int main(int argc, char* argv[])
{
size_t srcLen = BUF_SZ;
size_t destLen = BUF_SZ * 2;
unsigned char psrcRead[BUF_SZ*2]; //原始文件数据
unsigned char pDecomress[BUF_SZ*2]; //存放解压缩数据
unsigned char pLzma[BUF_SZ*2]; //存放压缩数据
unsigned char prop[5] =
{
0
};
size_t sizeProp = 5;
int idx;
memset(psrcRead, 0, BUF_SZ);
for(idx = 0; idx < 8192/10; idx++)
{
psrcRead[0 + idx*10] = 0x00;
psrcRead[1 + idx*10] = 0x11+idx;
psrcRead[2 + idx*10] = 0x22+idx;
psrcRead[3 + idx*10] = 0x33;
psrcRead[4 + idx*10] = 0x23;
psrcRead[5 + idx*10] = 0x25+idx;
psrcRead[6 + idx*10] = 0x01;
psrcRead[7 + idx*10] = 0x11;
psrcRead[8 + idx*10] = 0x01;
psrcRead[9 + idx*10] = 0x01;
}
if (SZ_OK != LzmaCompress(pLzma, &destLen, psrcRead, srcLen, prop,
&sizeProp, 9, (1 << 12), 3, 0, 2, 32, 2))
{
//出错了
return - 1;
}
memset(pDecomress, 0, BUF_SZ*2);
//注意:解压缩时props参数要使用压缩时生成的outProps,这样才能正常解压缩
if (SZ_OK != LzmaUncompress(pDecomress, &srcLen, pLzma, &destLen, prop, 5))
{
return - 1;
}
if(0 == memcmp(pDecomress, psrcRead, BUF_SZ))
{
printf("compress and uncompress succeeds.\r\n");
}
return 0;
}
zlib使用:
zlib 是通用的压缩库,提供了一套 in-memory 压缩和解压函数,并能检测解压出来的数据的完整性(integrity)。下面介绍两个最有用的函数——compress 和 uncompress。
int compress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen);
compress函数将 source 缓冲区中的内容压缩到 dest 缓冲区。 sourceLen 表示source 缓冲区的大小(以字节计)。注意函数的第二个参数 destLen 是传址调用。当调用函数时,destLen表示 dest 缓冲区的大小,destLen > (sourceLen + 12)*100.1%。当函数退出后,destLen 表示压缩后缓冲区的实际大小。此时 destLen / sourceLen 正好是压缩率。
compress 若成功,则返回 Z_OK;若没有足够内存,则返回 Z_MEM_ERROR;若输出缓冲区不够大,则返回 Z_BUF_ERROR。
int uncompress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen);
uncompress 函数将 source 缓冲区的内容解压缩到 dest 缓冲区。sourceLen 是 source 缓冲区的大小(以字节计)。注意函数的第二个参数 destLen 是传址调用。当调用函数时,destLen 表示 dest 缓冲区的大小, dest 缓冲区要足以容下解压后的数据。在进行解压缩时,需要提前知道被压缩的数据解压出来会有多大。这就要求在进行压缩之前,保存原始数据的大小(也就是解压后的数据的大小)。这不是 zlib 函数库的功能,需要我们做额外的工作。当函数退出后,
destLen
是解压出来的数据的实际大小。
uncompress 若成功,则返回 Z_OK ;若没有足够内存,则返回 Z_MEM_ERROR;若输出缓冲区不够大,则返回 Z_BUF_ERROR。若输入数据有误,则返回 Z_DATA_ERROR。
代码如下:
#include "stdafx.h"
#include <cstring>
#include <cstdlib>
#include <iostream>
#include "zlib.h"
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
int err = 0;
Byte compr[200] = {0}, uncompr[200] = {0}; // big enough
uLong comprLen = 0, uncomprLen = 0;
const char* hello = "12345678901234567890123456789012345678901234567890";
uLong len = strlen(hello) + 1;
comprLen = sizeof(compr) / sizeof(compr[0]);
err = compress(compr, &comprLen, (const Bytef*)hello, len);
if (err != Z_OK)
{
cerr << "compess error: " << err << '\n';
exit(1);
}
cout << "orignal size: " << len
<< " , compressed size : " << comprLen << '\n';
strcpy((char*)uncompr, "garbage");
err = uncompress(uncompr, &uncomprLen, compr, comprLen);
if (err != Z_OK)
{
cerr << "uncompess error: " << err << '\n';
exit(1);
}
cout << "orignal size: " << len
<< " , uncompressed size : " << uncomprLen << '\n';
if (strcmp((char*)uncompr, hello))
{
cerr << "BAD uncompress!!!\n";
exit(1);
}
else
{
cout << "uncompress() succeed: \n" << (char*)uncompr;
}
}
相关文章推荐
- 嵌入式目标板程序的压缩(1)--学习使用LZMA SDK
- 嵌入式目标板程序的压缩(2)--学习python脚本,测试LZMA
- 嵌入式学习之路(四)——使用vi编写c程序
- 使用u-boot的USB下载功能烧写程序到Nand Flash ——韦东山嵌入式Linux学习笔记06
- 嵌入式目标板程序的压缩(3)--实现目标板解压升级程序功能
- 使用u-boot的tftp下载功能烧写程序到Nand Flash ——韦东山嵌入式Linux学习笔记09
- c#写的五子棋程序,供学习WinForms的鼠标事件和使用GDI+,源码下载。
- 【转贴】在Solaris中使用用户态程序直接访问I/O端口 - 上海嵌入式家园 贺工
- 程序测试程序(配合游戏及学习控制管理器使用)
- 使用SharpZipLib制作压缩解压缩程序
- 学习API HOOK,编写了一个winsock 的封包抓取程序,可免费使用;
- 学习使用PathGradientBrush的小程序
- 【嵌入式Linux学习七步曲之第二篇 ARM+Linux开发环境】gdb+gdbserver的方式进行ARM程序调试
- 重新学习python线程应用使用thread类,改写程序
- 使用Java语言理解程序逻辑指导学习1——万年历
- .NET使用一般处理程序生成验证码(网摘学习)
- 使用嵌入式CPU对FPGA进行SelectMAP加载配置程序
- 使用嵌入式 Tomcat 简化程序调试
- 学习Java6(七)Desktop(3)使用默认程序打开所选文件
- 在jsp程序中使用com组件(资料学习)