黑马程序员技术交流社区
标题: 【济南中心】Android课程同步笔记day14:Android应用之安全卫士 [打印本页]
作者: 小鲁哥哥 时间: 2017-4-2 11:43
标题: 【济南中心】Android课程同步笔记day14:Android应用之安全卫士
本帖最后由 小鲁哥哥 于 2017-4-12 14:35 编辑
【济南中心】Android课程同步笔记day14:Android应用之安全卫士
上方扫描过程中一个进度条展示,下方一个listview展示扫描中的数据,最后扫描完毕上方显示结果。
整体的界面分成两部分,上方进度和结果展示,下方具体数据展示,用一个listview即可。
所以可以先将上方布局空出来不实现,先实现下方的listview。
前期准备Listview数据展示这一块大家都很熟悉了,主要数据分析这块,如何判断当前应用程序是否是病毒文件,这里可以通过判断文件的MD5值进行比对,市面上做安全软件的公司都有自己独立的病毒数据库样本,所以这里我们给大家准备了一个病毒数据库,只需要将数据进行拷贝即可。
数据库文件只有175kb比较小,不需要进行gizp压缩了,直接拷贝到我们的手机上即可,这里在SplashActivity中进行数据库的拷贝:
[Java] 纯文本查看 复制代码
private void copyAntivirusDB() {
File file = new File(getFilesDir(), "antivirus.db");
if (file.exists()) {
return;
}
AssetManager assets = getAssets();
InputStream stream = null;
FileOutputStream fos = null;
try {
stream = assets.open("antivirus.db");
fos = new FileOutputStream(file);
int len = -1;
byte[] buffer = new byte[1024];
while ((len = stream.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
StreamUtils.closeIO(stream);
StreamUtils.closeIO(fos);
}
}
实现数据的dao操作,主要是通过md5值查询当前数据库中是否有对应的数据:
[Java] 纯文本查看 复制代码
public class AntivirusDao {
/**
* 判断是否是病毒文件
* @param context
* @param md5:文件的md5值
* @return
*/
public static boolean isVirus(Context context, String md5) {
String path = new File(context.getFilesDir(), "antivirus.db")
.getAbsolutePath();
SQLiteDatabase db = SQLiteDatabase.openDatabase(path, null,
SQLiteDatabase.OPEN_READONLY);
String sql = "select count(_id) from datable where md5=?";
Cursor cursor = db.rawQuery(sql, new String[] { md5 });
int count = 0;
if (cursor != null) {
if (cursor.moveToNext()) {
count = cursor.getInt(0);
}
cursor.close();
}
db.close();
return count > 0;
}
}
接着需要自己实现或者拷贝一个获取文件md5值的工具类,代码如下:
[Java] 纯文本查看 复制代码
public class MD5Utils {
/**
* 获取文件的md5指纹
*
* @param file
* @return
*/
public static String md5(File file) {
FileInputStream in = null;
// MD5
try {
in = new FileInputStream(file);
MessageDigest digester = MessageDigest.getInstance("MD5");
byte[] bytes = new byte[8192];
int byteCount;
while ((byteCount = in.read(bytes)) > 0) {
digester.update(bytes, 0, byteCount);
}
// 文件指纹
byte[] digest = digester.digest();
// 转换byte --》String
StringBuilder builder = new StringBuilder();
for (byte b : digest) {
int c = b & 0xff;// 16进制的数据
// 0,1,2....9,A,B,C,D,E,F
String str = Integer.toHexString(c);
if (str.length() == 1) {
str = 0 + str;
}
builder.append(str);
}
return builder.toString();
} catch (Exception e) {
e.printStackTrace();
} finally {
StreamUtils.closeIO(in);
}
return null;
}
public static String md5(InputStream in) {
// MD5
try {
MessageDigest digester = MessageDigest.getInstance("MD5");
byte[] bytes = new byte[8192];
int byteCount;
while ((byteCount = in.read(bytes)) > 0) {
digester.update(bytes, 0, byteCount);
}
// 文件指纹
byte[] digest = digester.digest();
// 转换byte --》String
StringBuilder builder = new StringBuilder();
for (byte b : digest) {
int c = b & 0xff;// 16进制的数据
// 0,1,2....9,A,B,C,D,E,F
String str = Integer.toHexString(c);
if (str.length() == 1) {
str = 0 + str;
}
builder.append(str);
}
return builder.toString();
} catch (Exception e) {
e.printStackTrace();
} finally {
StreamUtils.closeIO(in);
}
return null;
}
}
到这儿,工具类,以及数据库都已经准备好了,接下来就是实现数据的展示了。
界面下方ListView的实现界面下方ListView的实现跟以前的一样,按照步骤快速实现,这里直接给出具体代码:
1. Activity布局xml:
[XML] 纯文本查看 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
style="@style/titleBarStyle"
android:text="手机杀毒" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="@color/title_bg" >
</RelativeLayout>
<ListView
android:id="@+id/antivirus_listview"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>
</LinearLayout>
2. 代码实现AntivirusActivity:
[Java] 纯文本查看 复制代码
public class AntivirusActivity extends Activity {
private ListView mListView;
private ArrayList<AntivirusBean> mDatas;
private AntivirusAdapter mAdapter;
private PackageManager mPm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_antivirus);
mPm = getPackageManager();
initView();
initData();
}
private void initView() {
mListView = (ListView) findViewById(R.id.antivirus_listview);
}
private void initData() {
mDatas = new ArrayList<AntivirusBean>();
mAdapter = new AntivirusAdapter();
mListView.setAdapter(mAdapter);
startScan();
}
private void startScan() {
new AsyncTask<Void, AntivirusBean, Void>() {
@Override
protected void onPreExecute() {
// 清空数据
mDatas.clear();
mAdapter.notifyDataSetChanged();
};
@Override
protected Void doInBackground(Void... params) {
List<PackageInfo> packages = mPm.getInstalledPackages(0);
for (PackageInfo info : packages) {
String apkPath = info.applicationInfo.sourceDir;
File file = new File(apkPath);
String md5 = MD5Utils.md5(file);
AntivirusBean bean = new AntivirusBean();
bean.icon = PackageUtils.getAppIcon(AntivirusActivity.this,
info);
bean.name = PackageUtils.getAppName(AntivirusActivity.this,
info);
bean.isVirus = AntivirusDao.isVirus(AntivirusActivity.this,
md5);// 如何判断一个应用是否是病毒
bean.packageName = info.packageName;
// 推送到主线程
publishProgress(bean);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
@Override
protected void onProgressUpdate(AntivirusBean... values) {
AntivirusBean bean = values[0];
// 边添加边UI更新
if (bean.isVirus) {
// 是病毒
mDatas.add(0, bean);
} else {
mDatas.add(bean);
}
mAdapter.notifyDataSetChanged();
// listview滚动到底部
mListView.smoothScrollToPosition(mAdapter.getCount());
}
@Override
protected void onPostExecute(Void result) {
// 滚动到顶部
mListView.smoothScrollToPosition(0);
};
}.execute();
}
private class AntivirusAdapter extends BaseAdapter {
@Override
public int getCount() {
if (mDatas != null) {
return mDatas.size();
}
return 0;
}
@Override
public Object getItem(int position) {
if (mDatas != null) {
return mDatas.get(position);
}
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = View.inflate(AntivirusActivity.this,
R.layout.item_antivirus, null);
holder = new ViewHolder();
convertView.setTag(holder);
holder.ivIcon = (ImageView) convertView
.findViewById(R.id.item_antivirus_iv_icon);
holder.ivClear = (ImageView) convertView
.findViewById(R.id.item_antivirus_iv_clear);
holder.tvName = (TextView) convertView
.findViewById(R.id.item_antivirus_tv_name);
holder.tvVirus = (TextView) convertView
.findViewById(R.id.item_antivirus_tv_virus);
} else {
holder = (ViewHolder) convertView.getTag();
}
// 设置数据
final AntivirusBean bean = mDatas.get(position);
if (bean.icon == null) {
holder.ivIcon.setImageResource(R.drawable.ic_default);
} else {
holder.ivIcon.setImageDrawable(bean.icon);
}
holder.tvName.setText(bean.name);
holder.tvVirus.setText(bean.isVirus ? "病毒" : "安全");
holder.tvVirus.setTextColor(bean.isVirus ? Color.RED : Color.GREEN);
holder.ivClear.setVisibility(bean.isVirus ? View.VISIBLE
: View.GONE);
holder.ivClear.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// <intent-filter>
// <action android:name="android.intent.action.VIEW" />
// <action android:name="android.intent.action.DELETE" />
// <category android:name="android.intent.category.DEFAULT"
// />
// <data android:scheme="package" />
// </intent-filter>
Intent intent = new Intent();
intent.setAction("android.intent.action.DELETE");
intent.addCategory("android.intent.category.DEFAULT");
intent.setData(Uri.parse("package:" + bean.packageName));
startActivity(intent);
}
});
return convertView;
}
}
private static class ViewHolder {
ImageView ivIcon;
ImageView ivClear;
TextView tvName;
TextView tvVirus;
}
}
病毒数据的准备接下来我们需要准备一个病毒数据apk来进行测试,可以直接使用我们资料里面的12_病毒程序,导入你们的项目里面直接运行即可,运行起来后会在你的bin目录下生成一个apk文件,注意!每个人的电脑上的eclipse运行起来的apk文件md5值都是不一样的,所以运行完毕后,需要计算出这个apk文件的md5值:
将这个apk文件直接拷贝到你的安全卫士assets目录下,然后改名为12;
编写一个测试类进行计算md5值;
[Java] 纯文本查看 复制代码
public class TestMD5 extends AndroidTestCase {
private static final String TAG = "TestMD5";
public void testMd5() {
AssetManager assets = getContext().getAssets();
//73b057b0867c6bd0189a12890c6d8cd4
//99fc32cf7474921e86b7464ecabf767c
try {
InputStream open = assets.open("12.apk");
String md5 = MD5Utils.md5(open);
Log.d(TAG, "" + md5);
} catch (IOException e) {
e.printStackTrace();
}
}
}
比如我这里打印出来的值是99fc32cf7474921e86b7464ecabf767c,而视频中打印出来的值是73b057b0867c6bd0189a12890c6d8cd4,你需要将这个MD5值插入到你的数据库中去;
记得修改完数据库后替换掉你项目下的数据库文件,并且删除掉你data/data/包名/files/ 的数据库。
插入完毕后,删除掉assets下的12 apk程序,重新运行程序:
上方UI的展示首先在之前空出来未实现的上方布局中我们去实现好它需要展示的界面,其中扫描的时候有一个进度圈的展示,我们这里用到了一个三方的框架CircleProgressLibrary,将它导入工程,并引用这个library。
上方布局的实现:
[XML] 纯文本查看 复制代码
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="@color/title_bg" >
<!-- 扫描中 -->
<RelativeLayout
android:id="@+id/antivirus_rl_container_scanning"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- 进度: 临时显示 -->
<com.github.lzyzsd.circleprogress.ArcProgress
android:id="@+id/antiviru_arc_progress"
android:layout_width="130dp"
android:layout_height="130dp"
android:layout_centerInParent="true"
custom:arc_bottom_text="扫描中"
custom:arc_bottom_text_size="10dp"
custom:arc_progress="55"
custom:arc_stroke_width="10dp"
custom:arc_text_color="#ffffff" />
<!-- 扫描的包名 -->
<TextView
android:id="@+id/antiviru_tv_pkg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="4dp"
android:text="包名"
android:textColor="#ffffff"
android:textSize="14sp" />
</RelativeLayout>
<!-- 扫描后 -->
<RelativeLayout
android:id="@+id/antivirus_rl_container_scanned"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical" >
<TextView
android:id="@+id/antiviru_tv_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="4dp"
android:text="扫描结果"
android:textColor="#ffffff"
android:textSize="16sp" />
<Button
android:id="@+id/antiviru_btn_scan"
style="@style/btnOkNormal"
android:text="重新扫描" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
代码中找到控件并做初始化。
在程序扫描的过程中需要进行一些进度圈和文本的展示的操作,主要涉及到修改的代码如下:
在AsyncTask中,首先是在准备阶段需要显示扫描的控件,隐藏扫描结束的控件:
mVirusCount代表的是病毒的个数。
接着是在扫描的过程中统计下应用程序的个数,方便设置进度条最大进度。
然后在onProgressUpdate方法中进行UI的更新:
最后在扫描结束的方法中隐藏扫描的控件,显示扫描结束的控件:
记得给扫描结束后的按钮设置点击事件,当点击重新扫描执行startScan方法进行扫描。
动画的展示修改AntivrusActivity的布局,添加两个ImageView用于做动画;
[XML] 纯文本查看 复制代码
<!-- 动画容器 -->
<LinearLayout
android:id="@+id/antivirus_ll_container_animation"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<ImageView
android:id="@+id/antivirus_iv_left"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@drawable/itcast" />
<ImageView
android:id="@+id/antivirus_iv_right"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@drawable/itheima" />
</LinearLayout>
代码中初始化控件,并且在AsyncTask准备阶段隐藏掉该动画控件:
在结束阶段显示动画控件,并且获取到扫描中控件的显示图片,然后拆分成两块进行动画的实现:
[Java] 纯文本查看 复制代码
@Override
protected void onPostExecute(Void result) {
// 显示扫描后,隐藏扫描中
mRlContainerScanned.setVisibility(View.VISIBLE);
mRlContainerScanning.setVisibility(View.GONE);
mLlContainerAnimation.setVisibility(View.VISIBLE);
// 滚动到顶部
mListView.smoothScrollToPosition(0);
// 显示病毒结果
String text = "";
if (mVirusCount == 0) {
text = "您的手机很安全";
} else {
text = "您的手机有" + mVirusCount + "个病毒,请您杀死";
}
mTvResult.setText(text);
// 获取扫描中容器显示的图片,拆分成两块,分别给动画view去做动画
mRlContainerScanning.setDrawingCacheEnabled(true);
mRlContainerScanning
.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
Bitmap bitmap = mRlContainerScanning.getDrawingCache();
// 拆分
Bitmap leftBitmap = getLeftBitmap(bitmap);
mIvLeft.setImageBitmap(leftBitmap);
Bitmap rightBitmap = getRightBitmap(bitmap);
mIvRight.setImageBitmap(rightBitmap);
// 要显示扫描后打开的动画
startOpenAnimation();
};
/**
* 获取左半边图片
* @param bitmap
* @return
*/
private Bitmap getLeftBitmap(Bitmap bitmap) {
// 多媒体知识
// canvas
int width = (int) (bitmap.getWidth() / 2f + 0.5f);
int height = bitmap.getHeight();
Bitmap copyBitmap = Bitmap.createBitmap(width, height,
bitmap.getConfig());
// 准备canvas
Canvas canvas = new Canvas(copyBitmap);
// 准备矩阵
Matrix matrix = new Matrix();
// 准备画笔
Paint paint = new Paint();
// 画图
canvas.drawBitmap(bitmap, matrix, paint);
return copyBitmap;
}
/**
* 获取右半边图片
* @param bitmap
* @return
*/
private Bitmap getRightBitmap(Bitmap bitmap) {
// 多媒体知识
// canvas
int width = (int) (bitmap.getWidth() / 2f + 0.5f);
int height = bitmap.getHeight();
Bitmap copyBitmap = Bitmap.createBitmap(width, height,
bitmap.getConfig());
// 准备canvas
Canvas canvas = new Canvas(copyBitmap);
// 准备矩阵
Matrix matrix = new Matrix();
matrix.setTranslate(-width, 0);
// 准备画笔
Paint paint = new Paint();
// 画图
canvas.drawBitmap(bitmap, matrix, paint);
return copyBitmap;
}
/**
* 打开的动画
*/
private void startOpenAnimation() {
int leftWidth = mIvLeft.getWidth();
int rightWidth = mIvRight.getWidth();
// 让左侧图片向左做位移动画,透明度
// 让右侧图片向右做位移动画,透明度
// 让扫描结束后的容器做透明度动画
// 动画集合
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(mIvLeft, "translationX", 0, -leftWidth),
ObjectAnimator.ofFloat(mIvLeft, "alpha", 1f, 0),
ObjectAnimator.ofFloat(mIvRight, "translationX", 0, rightWidth),
ObjectAnimator.ofFloat(mIvRight, "alpha", 1f, 0),
ObjectAnimator.ofFloat(mRlContainerScanned, "alpha", 0, 1f));
set.setDuration(3000);
set.addListener(new AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// 让重新扫描的按钮不可用
mBtnScan.setEnabled(false);
}
@Override
public void onAnimationRepeat(Animator animation) {
// TODO Auto-generated method stub
}
@Override
public void onAnimationEnd(Animator animation) {
// 让重新扫描的按钮可用
mBtnScan.setEnabled(true);
}
@Override
public void onAnimationCancel(Animator animation) {
// TODO Auto-generated method stub
}
});
set.start();
}
/**
* 结束的动画
*/
private void startCloseAnimation() {
int leftWidth = mIvLeft.getWidth();
int rightWidth = mIvRight.getWidth();
// 让左侧图片向左做位移动画,透明度
// 让右侧图片向右做位移动画,透明度
// 让扫描结束后的容器做透明度动画
// 动画集合
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(mIvLeft, "translationX", -leftWidth, 0),
ObjectAnimator.ofFloat(mIvLeft, "alpha", 0f, 1f),
ObjectAnimator.ofFloat(mIvRight, "translationX", rightWidth, 0),
ObjectAnimator.ofFloat(mIvRight, "alpha", 0, 1f),
ObjectAnimator.ofFloat(mRlContainerScanned, "alpha", 1f, 0f));
set.setDuration(3000);
set.addListener(new AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// 让重新扫描的按钮不可用
mBtnScan.setEnabled(false);
}
@Override
public void onAnimationRepeat(Animator animation) {
// TODO Auto-generated method stub
}
@Override
public void onAnimationEnd(Animator animation) {
startScan();
}
@Override
public void onAnimationCancel(Animator animation) {
// TODO Auto-generated method stub
}
});
set.start();
}
细节处理1. 为了防止在扫描正在进行中用户突然退出当前界面或者按了home键导致的一些界面异常或崩溃,这里在doInBackground子线程实现的过程中加一个boolean的变量进行中断:
当界面失去焦点的时候停止
2. 在点击重新扫描按钮的过程中,不能再重复的让该按钮可以被点击,也就是在打开,关闭动画的过程中监听动画开始的时候将控件enab设置为flase即可,动画执行结束再将控件enable设置为true即可。在刚才上述动画代码中已经实现。
快捷图标给桌面上生成一个快捷图标:
在splashActivity中添加一下一段代码:
[Java] 纯文本查看 复制代码
private void createShortcut() {
// <receiver
// android:name="com.android.launcher2.InstallShortcutReceiver"
// android:permission="com.android.launcher.permission.INSTALL_SHORTCUT">
// <intent-filter>
// <action android:name="com.android.launcher.action.INSTALL_SHORTCUT"
// />
// </intent-filter>
// </receiver>
if (PreferenceUtils.getBoolean(this, Config.KEY_SHORTCUT)) {
return;
}
Intent intent = new Intent();
intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
// 通过意图带数据
// 1. 显示图标图案
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, BitmapFactory
.decodeResource(getResources(), R.drawable.ic_launcher));
// 2. 显示图标名称
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, "手机卫士");
// 3. 快捷图标点击行为
Intent clickIntent = new Intent();// 隐式意图
clickIntent.setAction("com.itheima.safe.home");
clickIntent.addCategory("android.intent.category.DEFAULT");
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, clickIntent);
sendBroadcast(intent);
PreferenceUtils.setBoolean(this, Config.KEY_SHORTCUT, true);
}
以上代码就是发送一个广播给系统,让系统去帮我们创建一个快捷图标,不过要记得通过sp去存储是否已经创建过图标,否则会创建多次。
由于上诉代码设置的是点击快捷图标打开HomeActivity,所以
清单文件中将HomeActivity进行修改:
[XML] 纯文本查看 复制代码
<activity
android:name="com.itheima.zphuanlove.activity.HomeActivity"
android:launchMode="singleTask" >
<intent-filter>
<action android:name="com.itheima.safe.home" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
自定义的Logger日志自定义logger日志可以控制我们日志的打印,以及是否打印日志:
[Java] 纯文本查看 复制代码
public class Logger {
/**
* Priority constant for the println method; use Log.v.
*/
public static final int VERBOSE = 2;
/**
* Priority constant for the println method; use Log.d.
*/
public static final int DEBUG = 3;
/**
* Priority constant for the println method; use Log.i.
*/
public static final int INFO = 4;
/**
* Priority constant for the println method; use Log.w.
*/
public static final int WARN = 5;
/**
* Priority constant for the println method; use Log.e.
*/
public static final int ERROR = 6;
/**
* Priority constant for the println method.
*/
public static final int ASSERT = 7;
private static final int mCurrentLevel = VERBOSE;
private static final boolean mEnable = true;
public static void v(String tag, String msg) {
if (!mEnable) {
return;
}
if (mCurrentLevel <= VERBOSE) {
Log.v(tag, msg);
}
}
public static void d(String tag, String msg) {
if (!mEnable) {
return;
}
if (mCurrentLevel <= DEBUG) {
Log.d(tag, msg);
}
}
public static void i(String tag, String msg) {
if (!mEnable) {
return;
}
if (mCurrentLevel <= INFO) {
Log.i(tag, msg);
}
}
public static void w(String tag, String msg) {
if (!mEnable) {
return;
}
if (mCurrentLevel <= WARN) {
Log.w(tag, msg);
}
}
public static void e(String tag, String msg) {
if (!mEnable) {
return;
}
if (mCurrentLevel <= ERROR) {
Log.w(tag, msg);
}
}
public static void wtf(String tag, String msg) {
if (!mEnable) {
return;
}
if (mCurrentLevel <= ASSERT) {
Log.wtf(tag, msg);
}
}
}
作者: Hp_Yx 时间: 2017-4-2 12:20
已收藏
作者: baby14 时间: 2019-8-7 07:43
多谢分享
欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) |
黑马程序员IT技术论坛 X3.2 |