需求:
Android端写一个界面,作为TCP服务端,接受客户端发来的图片以及一些信息,显示在界面上。再次打开APP的时候保证上一次图片存在。
思路:
1 编写一个TCP服务端,继承runnable接口的方式去实现,然后写一个接口回调监听TCP接受的数据。
2 主界面监听TCP服务的接口,背景图是一个ImgView,加载使用Bitmap
3 保存图片以及本地数据:文字类的使用sp存储,图片保存在SDCard下,使用File类操作
主要涉及的知识点:
1 TCP
2 线程的创建方法以及优缺点
3 Bitmap的使用
4 Android中操作SD卡
实现以及总结
package com.example.namebrand.network;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* 2023/05/13
*/
public class TCPServer implements Runnable {
private static final String TAG = "TCPServer";
private String chaSet = "UTF-8";
private int port;
private boolean isListen = true;
public TCPServer(int port) {
this.port = port;
}
byte[] imgBytes;
@Override
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(port);
Log.d(TAG, "run:等待客户端连接... ");
//serverSocket.setSoTimeout(2000);
while (isListen) {
Socket socket = serverSocket.accept();
Log.d(TAG, "run: 客户端已连接");
if (socket != null) {
acceptData(socket);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void acceptData(Socket socket) {
InputStream is;
OutputStream os;
int postX = 0;
int postY = 0;
int size = 0;
int reserveOne = 0;
int reserveTwo = 0;
String reserve = "";
int color = 0;
long pngLen = 0L;
int i = 0;
byte[] bytesBuffer = null;
int bufferPos = 0;
int rcvLen = 0;
try {
is = socket.getInputStream();
os = socket.getOutputStream();
byte[] bytes = new byte[1024 * 4];
while (!socket.isClosed() && !socket.isInputShutdown()) {
while ((rcvLen = is.read(bytes)) != -1) {
if (rcvLen > 0) {
if (i == 0) {
byte[] content = new byte[bytes.length];
System.arraycopy(bytes, 0, content, 0, bytes.length);
String res = new String(content, chaSet);
String trim = res.trim(); //打印的时候去掉多余部分
Log.d(TAG, "accept: len: " + rcvLen + " content:" + trim);
postX = getInt(bytes, 0, 4);
postY = getInt(bytes, 4, 4);
size = getInt(bytes, 8, 4);
reserveOne = getInt(bytes, 12, 4);
reserveTwo = getInt(bytes, 16, 4);
reserve = getString(bytes, 20, 56);
color = getInt(bytes, 76, 4);
pngLen = getInt(bytes, 80, 4);
if (pngLen > 0) {
bytesBuffer = new byte[1024 * 1024 * 5];
}
i++;
}
//再无图片数据
if (pngLen <= 0) {
if (onReceiveListener != null) {
onReceiveListener.receive(postX, postY, size, reserveOne, reserveTwo, reserve, color, pngLen,imgBytes);
}
i = 0;
} else {
System.arraycopy(bytes, 0, bytesBuffer, bufferPos, rcvLen);
bufferPos += rcvLen;
Log.d(TAG, "accept: bufferPos:" + bufferPos + "pngLen:" + pngLen + "rvcLen:" + rcvLen);
if (bufferPos >= pngLen + 84) {
imgBytes = new byte[bufferPos - 84];
System.arraycopy(bytesBuffer, 84, imgBytes, 0, bufferPos - 84);
if (onReceiveListener != null) {
onReceiveListener.receive(postX, postY, size, reserveOne, reserveTwo, reserve, color, pngLen,imgBytes);
}
bufferPos = 0;
pngLen = 0;
i = 0;
}
}
}
}
}
socket.close();
Log.d(TAG, "accept: 断开连接");
} catch (IOException e) {
e.printStackTrace();
}
}
//byte转int
public int getInt(byte[] srcBytes, int srcPos, int length) {
byte[] bytes = new byte[length];
System.arraycopy(srcBytes, srcPos, bytes, 0, length);
int anInt = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
return anInt;
}
//byte转String
public String getString(byte[] srcBytes, int srcPos, int length) {
byte[] bytes = new byte[length];
System.arraycopy(srcBytes, srcPos, bytes, 0, length);
String str = "解析错误";
try {
str = new String(bytes, 0, length, chaSet);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return str.trim();
}
private onReceiveListener onReceiveListener;
public interface onReceiveListener {
void receive(int nPostX, int nPostY, int nSize, int nReserveOne, int nReserveTwo, String reserve, int color, long pngLen,byte[] bytes);
}
public void setOnReceiveListener(onReceiveListener onReceiveListener) {
this.onReceiveListener = onReceiveListener;
}
}
这里使用的是Runnable接口来进行创建线程,主要逻辑操作都封装在run方法中,创建一个socket,然后接收数据,由于提前定义好了数据结构,然后我在accpetData中取出对应长度的字节数组,并且根据数据类型转换成Int或者String,由于最后传入的时图片的长度,那么除了前面的84字节,后面的都是图片的内容,然后就把后面接收到的所有字节拼接成一个新的字节数组,这就是图片。
然后就是byte转int和byte转String的方法。
接下来就是定义接口和回调,在上面接收数据的时候,如果再没有图片字节数组,就代表已经传递完毕,调用receive方法将已经接收的数据回调到页面。
在程序启动后,开启进行接收数据的监听:
@Override
protected void onResume() {
super.onResume();
tcpServer.setOnReceiveListener(new TCPServer.onReceiveListener() {
@Override
public void receive(int nPostX, int nPostY, int nSize, int nReserveOne, int nReserveTwo, String reserve, int color, long pngLen, byte[] bytes) {
Log.d(TAG, "receive\n postX:" + nPostX + "\npostY:" + nPostY + "\nSize:" + nSize + "\nnReserveOne:"
+ nReserveOne + "\nnReserveTwo:" + nReserveTwo + "\nreserve:" + reserve + "\ncolor:" + color + "\npngLen:" + pngLen + "\nbytes:" + bytes);
//子线程中获取收到的信息 用handler发送给主线程
BrandBean brandBean = new BrandBean(nPostX, nPostY, nSize, nReserveOne, nReserveTwo, reserve, color, bytes);
Message message = Message.obtain();
message.what = 0;
Bundle bundle = new Bundle();
bundle.putParcelable("brand", brandBean);
message.setData(bundle);
handler.sendMessage(message);
}
});
}
接收到的数据通过handler发送给主线程,更新UI操作
//主线程用handler接收数据 更新UI(背景+时间)
final Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
Bundle bundle = msg.getData();
BrandBean brand = bundle.getParcelable("brand");
//更新UI
UpdateBg(brand.getTimeX(), brand.getTimeY(), brand.getTimeSize(), brand.getReceiveOne(), brand.getReceiveTwo(), brand.getReceive(), brand.getTimeColor(), brand.getBgImg());
}
}
};
接下来是字节数组转图片,我用的是Bitmap类来进行操作。来介绍下Bitmap,在 Android 中,Bitmap是用于表示图像的类,提供了各种方法和功能来加载、创建、操作和显示图像。下面是对 Bitmap的使用进行详细解释:
加载图像:
- 通过资源加载:使用
BitmapFactory
类的decodeResource()
方法从资源中加载图像。- 通过文件加载:使用
BitmapFactory
类的decodeFile()
方法从文件中加载图像。- 通过字节数组加载:使用
BitmapFactory
类的decodeByteArray()
方法从字节数组中加载图像。- 通过流加载:使用
BitmapFactory
类的decodeStream()
方法从输入流中加载图像。创建图像:
- 使用
Bitmap.createBitmap()
方法创建空白的Bitmap
对象。- 使用
Bitmap.createScaledBitmap()
方法创建按比例缩放的Bitmap
对象。- 使用
Bitmap.createBitmap(int width, int height, Bitmap.Config config)
方法创建指定尺寸和配置的Bitmap
对象。图像操作和处理:
- 裁剪图像:使用
Bitmap.createBitmap()
方法创建裁剪后的新Bitmap
对象。- 缩放图像:使用
Bitmap.createScaledBitmap()
方法创建缩放后的新Bitmap
对象。- 旋转图像:使用
Matrix
类的postRotate()
方法旋转图像,并使用Bitmap.createBitmap()
方法创建旋转后的新Bitmap
对象。- 应用滤镜效果:使用
ColorMatrix
和ColorMatrixColorFilter
类来调整图像颜色和应用滤镜效果。- 修改像素值:使用
setPixel()
和getPixel()
方法直接修改或获取图像的像素值。图像存储和读取:
- 保存图像:使用
compress()
方法将Bitmap
对象保存为指定格式的图像文件。- 读取图像:使用
decodeFile()
方法从文件中读取图像数据为Bitmap
对象。图像显示:
- 使用
ImageView
组件:将Bitmap
对象设置给ImageView
组件,通过setImageBitmap()
方法显示图像。- 使用
Canvas
绘制:使用Canvas
类的drawBitmap()
方法在Canvas
上绘制图像。内存管理和优化:
- 及时回收:通过调用
Bitmap.recycle()
方法及时回收不再需要的Bitmap
对象,释放内存资源。- 优化加载:通过设置
BitmapFactory.Options
对象的inSampleSize
字段来减小图像的尺寸,降低内存占用。- 图像缓存:使用图像缓存库(如 LruCache、DiskLruCache)进行图像的内存和
操作SD卡的相关代码如下:
/**
* 获取SD卡下程序的缓存路径
*/
public static File getCacheDir(Context context, String name) {
File cacheDir;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
cacheDir = context.getExternalCacheDir(); // 外部存储 需要手动清理 /sdcard/Android/data//cache/
} else {
cacheDir = context.getCacheDir(); // 应用内内存 /data/data//cache/
}
if (cacheDir != null) {
if (name != null) {
File cacheFile = new File(cacheDir.getAbsolutePath() + "/" + name);//当前cache所在的路径
Log.d(TAG, "getCacheDir:33 "+cacheFile);
if (createDir(cacheFile)) {
cacheDir = cacheFile;
Log.d(TAG, "getCacheDir: "+cacheDir);
} else {
cacheDir = null;
}
}
} else {
return null;
}
return cacheDir;
}
/**
* 创建指定文件夹
*
* @param cacheDir
* @return
*/
private static boolean createDir(File cacheDir) {
if (cacheDir == null) {
return false;
}
if (cacheDir.exists() && cacheDir.isDirectory()) {
return true;
} else {
return cacheDir.mkdir();
}
}
操作文件类写好了,bitmap进行图片的存储和读取也知道对应的方法了,下面就是进行文件夹下的图片的存储和读取:
/**
* 向文件夹写入bitmap
*/
public static boolean writeBitmap(File path, Bitmap bitmap) {
FileOutputStream fs = null;
Log.d(TAG, "writeBitmap: "+path);
try {
fs = new FileOutputStream(path.getAbsolutePath());
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fs); //将bitmap压缩成PNG形式写入文件夹
fs.flush();//刷新输出流 确保缓冲区的数据及时写入 不会丢失
return true;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fs != null) {
try {
fs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
/**
* 从指定文件夹下读取bitmap
*/
public static Bitmap readBitmap(File file) {
if (!file.exists()) {
return null;
} else {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
return bitmap;
}
}
在主线程的handler中接收数据进行更新UI的地方调用该方法即可,注意UI更新要在主线程中进行
new Thread(new Runnable() {
@Override
public void run() {
Bitmap fileBitmap = getFileBitmap(IMAGE_BG);
runOnUiThread(new Runnable() {
@Override
public void run() {
imgBg.setImageBitmap(fileBitmap);
}
});
}
}).start();
text View的显示位置:
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
layoutParams.leftMargin = x;
layoutParams.topMargin = y;
timeText.setTextSize(timeSize);