Android 测试数据存储与访问XML解析与生成

智慧并不产生于学历,而是来自对于知识的终生不懈的追求。这篇文章主要讲述Android 测试数据存储与访问XML解析与生成相关的知识,希望能为你提供帮助。
1.android测试
1.黑盒测试: 是以用户的角度, 从输入数据与输出数据的对应关系出发进行测试的。
2. 白盒测试: 又称结构测试、透明盒测试、逻辑驱动测试或基于代码的测试。
3.单元测试: 又称模块测试, 是开发者编写的一小段代码, 用于检验被测代码的一个很小的、很明确的功能是否正确。
4.功能测试: 根据产品特性、操作描述和用户方案, 测试一个产品的特性和可操作行为以确定它们满足设计需求。
5.压力测试: 主体向被观察者布置一定量任务和作业, 借以观察个体完成任务的行为。
6.集成测试: 是单元测试的逻辑扩展。它的最简单的形式是: 两个已经测试过的单元组合成一个组件, 并且测试它们之间的接口
压力测试:
monkey -p < 应用程序包名> -v 事件数量

对应用进行单元测试

在实际开发中, 开发android软件的过程需要不断地进行测试。而使用Junit测试框架, 侧是正规Android开发的必用技术, 在Junit中可以得到组件, 可以模拟发送事件和检测程序处理的正确性。 第一步: 首先在AndroidManifest.xml中加入下面红色代码: < manifest xmlns:android= " http://schemas.android.com/apk/res/android" package= " cn.itcast.action“ android:versionCode= " 1“android:versionName= " 1.0" > < application android:icon= " @ drawable/icon" android:label= " @ string/app_name" > **< uses-library android:name= " android.test.runner" /> ** .... < /application> < uses-sdk android:minSdkVersion= " 6" /> **< instrumentation android:name= " android.test.InstrumentationTestRunner" android:targetPackage= " cn.itcast.action" android:label= " Tests for My App" /> ** < /manifest> 上面targetPackage指定的包要和应用的package相同。 第二步: 编写单元测试代码( 选择要测试的方法, 右键点击“Run As”--“Android Junit Test” ) : import android.test.AndroidTestCase; import android.util.Log; public class XMLTest extends AndroidTestCase { public void testSomething() throws Throwable { Assert.assertTrue(1 + 1 = = 3); } }

2.数据存储与访问
1.文件
2.SharedPreferences(参数)
3.SQLite 数据库
4.Content provider 内容提供者
5.网络
**1,使用文件进行数据存储**

public class SaveFileService {// 存储/** * 存储操作 * * @ param username *帐号名 * @ param password *密码 * @ return */public static boolean saveFile(Context context, String username, String password){ // we do chicken right // context : 上下文, 就是为我们提供了一些简便的API // 返回的一个文件的绝对路径// 创建文件对象 // File file = new File(" /data/data/com.itheima.login" , " info.txt" ); // 创建文件对象, 通过file目录 // File file = new File(context.getFilesDir(), " info.txt" ); // 创建文件对象, 通过cache目录 File file = new File(context.getCacheDir(), " info.txt" ); try { // 文件输出流 FileOutputStream fos = new FileOutputStream(file); // 写数据,zhangsan##123 fos.write((username + " ##" + password).getBytes()); // 关闭输出流fos.close(); return true; } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); return false; }}// 回显数据public static Map< String, String> getUserInfo(Context context) {// 文件对象File file = new File(context.getCacheDir(), " info.txt" ); try { // 输入流 FileInputStream fis = new FileInputStream(file); // br对象 BufferedReader br = new BufferedReader(new InputStreamReader(fis)); // 读里面的内容,zhangsan##123 String result = br.readLine(); // 拆分result,得到的是数组 String[] results = result.split(" ##" ); // 得到map集合 Map< String, String> map = new HashMap< String, String> (); // 存储数据 map.put(" username" , results[0]); map.put(" password" , results[1]); return map; } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); return null; }}}

ublic class MainActivity extends Activity {private static final String TAG = " MainActivity" ; private EditText et_username; private EditText et_password; private CheckBox cb_remeber; @ Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 获取控件的对象 et_username = (EditText) findViewById(R.id.et_username); et_password = (EditText) findViewById(R.id.et_password); cb_remeber = (CheckBox) findViewById(R.id.cb_remeber); // 调用回显数据的方法Map< String, String> map = SaveFileService.getUserInfo(this); if (map != null) { // 接收返回的map集合 String username = map.get(" username" ); String password = map.get(" password" ); et_username.setText(username); et_password.setText(password); }}public void login(View v) { // 获取et控件内的内容 String username = et_username.getText().toString().trim(); String password = et_password.getText().toString().trim(); // 判断内容是否为空if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) { // 弹出吐死 Toast.makeText(this, " 帐户名或密码不能为空" , 0).show(); } else { // 判断帐户名和密码是否正确 if (" zhangsan" .equals(username) & & " 123" .equals(password)) { Toast.makeText(this, " 恭喜你! " , 0).show(); }}if (cb_remeber.isChecked()) { // 判断 boolean correct = SaveFileService .saveFile(this, username, password); if (correct) { Toast.makeText(this, " 存储成功" , 0).show(); } else { Toast.makeText(this, " 存储失败" , 0).show(); }}}}

(1)使用文件进行数据存储
在上下文中有一个方法叫openFileOutput()方法可以用于把数据输出到文件中, 具体的实现过程与在J2SE环境中保存数据到文件中是一样的。
FileOutputStream outStream = this.openFileOutput(“itcast.txt”, Context.MODE_PRIVATE);
outStream.write((username + “##” + password).getBytes());
outStream.close();
openFileOutput()方法的第一参数用于指定文件名称, 不能包含路径分隔符“/” , 如果文件不存在, Android会自动创建它。创建的文件保存在/data/data//files目录, 如: /data/data/cn.itcast/files/itcast.txt , 通过点击Eclipse菜单“Window”-“Show View”-“Other”, 在对话窗口中展开android文件夹, 选择下面的File Explorer视图, 然后在File Explorer视图中展开/data/data//files目录就可以看到该文件。
openFileOutput()方法的第二参数用于指定操作模式, 有四种模式, 分别为: Context.MODE_PRIVATE = 0
Context.MODE_APPEND = 32768
Context.MODE_WORLD_READABLE = 1
Context.MODE_WORLD_WRITEABLE = 2
ontext.MODE_PRIVATE: 为默认操作模式, 代表该文件是私有数据, 只能被应用本身访问, 在该模式下, 写入的内容会覆盖原文件的内容, 如果想把新写入的内容追加到原文件中。可以使用Context.MODE_APPEND
Context.MODE_APPEND: 模式会检查文件是否存在, 存在就往文件追加内容, 否则就创建新文件。
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。
MODE_WORLD_READABLE: 表示当前文件可以被其他应用读取;
MODE_WORLD_WRITEABLE: 表示当前文件可以被其他应用写入。
如果希望文件被其他应用读和写, 可以传入:
openFileOutput(“itcast.txt”, Context.MODE_WORLD_READABLE+ Context.MODE_WORLD_WRITEANLE
android有一套自己的安全模型, 当应用程序(.apk)在安装时系统就会分配给他一个userid, 当该应用要去访问其他资源比如文件的时候, 就需要userid匹配。默认情况下, 任何应用创建的文件, sharedpreferences, 数据库都应该是私有的( 位于/data/data//files) , 其他程序无法访问。除非在创建时指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE , 只有这样其他程序才能正确访问。
(2)读取文件内容
如果要打开存放在/data/data//files目录应用私有的文件, 可以使用Activity提供openFileInput()方法。
FileInputStream inStream = this.getContext().openFileInput(“itcast.txt”);
Log.i(“FileTest”, readInStream(inStream));
readInStream()的方法请看本页下面备注。
或者直接使用文件的绝对路径:
File file = new File(“/data/data/cn.itcast/files/itcast.txt”);
FileInputStream inStream = new FileInputStream(file);
Log.i(“FileTest”, readInStream(inStream));
注意: 上面文件路径中的“cn.itcast”为应用所在包, 当你在编写代码时应替换为你自己应用使用的包。
对于私有文件只能被创建该文件的应用访问, 如果希望文件能被其他应用读和写, 可以在创建文件时, 指定Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE权限。
Activity还提供了getCacheDir()和getFilesDir()方法:
getCacheDir()方法用于获取/data/data//cache目录
getFilesDir()方法用于获取/data/data//files目录
把文件存放在SDCard
要往SDCard存放文件, 程序必须先判断手机是否装有SDCard, 并且可以进行读写。
注意: 访问SDCard必须在AndroidManifest.xml中加入访问SDCard的权限
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
File sdCardDir = Environment.getExternalStorageDirectory(); //获取SDCard目录
File saveFile = new File(sdCardDir, “itcast.txt”);
FileOutputStream outStream = new FileOutputStream(saveFile);
outStream.write(“传智播客”.getBytes());
outStream.close();
}
Environment.getExternalStorageState()方法用于获取SDCard的状态, 如果手机装有SDCard, 并且可以进行读写, 那么方法返回的状态等于Environment.MEDIA_MOUNTED。
Environment.getExternalStorageDirectory()方法用于获取SDCard的目录, 当然要获取SDCard的目录, 你也可以这样写:
File sdCardDir = new File(“/mnt/sdcard”); //获取SDCard目录
File saveFile = new File(sdCardDir, “itcast.txt”);
//上面两句代码可以合成一句: File saveFile = new File(“/mnt/sdcard/itcast.txt”);
FileOutputStream outStream = new FileOutputStream(saveFile);
outStream.write((username + “##” + password).getBytes());
outStream.close();
**在程序中访问SDCard, 你需要申请访问SDCard的权限。
// 挂载和卸载SDCard
android.permission.MOUNT_UNMOUNT_FILESYSTEMS
// 写入外存储设备权限
android.permission.WRITE_EXTERNAL_STORAGE**
  1. StrutsStAFs ,
  2. Environment.getExternalStorageDirectory() 获取sd卡目录
  3. Environment.getExternalStorageState() 获取SD卡状态
  4. File file = Context.getFilesDir(); 获取SD卡大小
  5. file.getUsableSpace : 获取可用空间
  6. file.getTotalSpace : 获取总空间
    chmod 更改文件的权限
    • rw- rw- rw- 共有的
    • rw- rw- r– 可读的
    • rw- rw- -w- 可写的
    • rw- rw- — 私有的 默认
  7. adb shell —-> chmod 666 xx.txt 代表更改xx.txt的文件权限为 可读可写
    Android 测试数据存储与访问XML解析与生成

    文章图片

    Android 测试数据存储与访问XML解析与生成

    文章图片

    2.SharedPreferences
    (1)使用SharedPreferences进行数据存储
    使用SharedPreferences保存数据, 其背后是用xml文件存放数据, 文件存放在/data/data//shared_prefs目录下:
    SharedPreferences sharedPreferences = getSharedPreferences(“itcast”, Context.MODE_PRIVATE);
    Editor editor = sharedPreferences.edit(); //获取编辑器
    editor.putString(“name”, “小朋友”);
    editor.putInt(“age”, 4);
    editor.commit(); //提交修改
    生成的itcast.xml文件内容如下:
public class SaveFileService { public static boolean saveFile(Context context,String username,String password) { SharedPreferences sp = context.getSharedPreferences(" info" ,Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putString(" username" ,username); editor.putString(" password" ,password); editor.commit(); return true; } }

(2)访问SharedPreferences中的数据
如果想访问其他应用中的Preference。
有两个前提条件是:
两个应用程序需要在AndroidManifest.xml中manifest节点里添加sharedUserId属性, 并且要一样, 而且还要有两级, 也就是需要有个“.”
该preference创建时必须指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE权限。
满足上面两个条件之后在我们应用中需要得到另一个应用的上下文对象:
Context otherAppsContext = createPackageContext(“cn.itcast.action”, Context.CONTEXT_IGNORE_SECURITY);
拿到上下文对象之后通过调用上下文中的方法得到SharedPreferences对象, 最后进行数据的交互。
SharedPreferences sharedPreferences = otherAppsContext.getSharedPreferences(“itcast”, Context.MODE_WORLD_READABLE);
SharedPreferences sp= this.getSharedPreferences(" info" , Context.MODE_PRIVATE); String username = sp.getString(" username" ,null); String password = sp.getString(" password" ,null); et_username.setText(username); et_password.setText(password);

3.Sqlite 轻量级数据库
SQLite, 是一款轻量型的数据库, 是遵守ACID(原子性、一致性、隔离性、持久性)的关联式数据库管理系统, 多用于嵌入式开发中。
SQLite的数据类型: Typelessness(无类型), 可以保存任何类型的数据到你所想要保存的任何表的任何列中. 但它又支持常见的类型比如: NULL, VARCHAR, TEXT, INTEGER, BLOB, CLOB…等. 唯一的例外: integer primary key 此字段只能存储64位整数
在Android系统, 提供了一个SQLiteOpenHelper抽象类, 该类用于对数据库版本进行管理.该类中常用的方法:
onCreate 数据库创建时执行(第一次连接获取数据库对象时执行)
onUpgrade 数据库更新时执行(版本号改变时执行)
onOpen 数据库每次打开时执行(每次打开数据库时调用, 在 onCreate, onUpgrade方法之后)
/** * @ author andong * 创建,打开,管理数据库的帮助类 * * 数据库创建的位置: /data/data/包名/databases/itheima40.db */ public class PersonOpenHelper extends SQLiteOpenHelper {public PersonOpenHelper(Context context) { this(context, " itheima40.db" , null, 3); }/** * @ param context * @ param name 数据库文件名 * @ param factory 游标结果集工厂类, 如果需要自定义游标结果集, 给null使用默认的Cursor * @ param version 数据库的版本号, 必须大于等于1 */ public PersonOpenHelper(Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version); }/** * 数据库文件第一次创建时调用此方法. * * 初始化一些表: person */ @ Override public void onCreate(SQLiteDatabase db) { System.out.println(" PersonOpenHelper: onCreate" ); String sql = " create table person(_id integer primary key autoincrement, name varchar(20), age integer)" ; db.execSQL(sql); }/** * 当更新表时出发此方法, 删除, 添加表. * * 添加一列: balance * * @ param oldVersion 老版本号 * @ param newVersion 新版本号 */ @ Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { System.out.println(" PersonOpenHelper: onUpgrade, oldVersion= " + oldVersion + " , newVersion= " + newVersion); if(oldVersion = = 2 & & newVersion = = 3) { // 这一次更新的操作: 添加一列: balancedb.execSQL(" alter table person add column balance integer; " ); } }}

使用SQLiteDatabase操作SQLite数据库
SQLiteDatabase的rawQuery() 用于执行select语句, 使用例子如下: SQLiteDatabase db = ….;
Cursor cursor = db.rawQuery(“select * from person”, null);
while (cursor.moveToNext()) {
int personid = cursor.getInt(0); //获取第一列的值,第一列的索引从0开始
String name = cursor.getString(1); //获取第二列的值
int age = cursor.getInt(2); //获取第三列的值
}
cursor.close();
db.close();
rawQuery()方法的第一个参数为select语句; 第二个参数为select语句中占位符参数的值, 如果select语句没有使用占位符, 该参数可以设置为null。带占位符参数的select语句使用例子如下:
Cursor cursor = db.rawQuery(“select * from person where name like ? and age= ?”, new String[]{“%小朋友%”, “4”});
Cursor是结果集游标, 用于对结果集进行随机访问, 如果大家熟悉jdbc, 其实Cursor与JDBC中的ResultSet作用很相似。使用moveToNext()方法可以将游标从当前行移动到下一行, 如果已经移过了结果集的最后一行, 返回结果为false, 否则为true。另外Cursor 还有常用的moveToPrevious()方法( 用于将游标从当前行移动到上一行, 如果已经移过了结果集的第一行, 返回值为false, 否则为true ) 、moveToFirst()方法( 用于将游标移动到结果集的第一行, 如果结果集为空, 返回值为false, 否则为true ) 和moveToLast()方法( 用于将游标移动到结果集的最后一行, 如果结果集为空, 返回值为false, 否则为true ) 。
public class PersonDao2 {private PersonOpenHelper openHelper; public PersonDao2(Context context) { openHelper = new PersonOpenHelper(context); }public void insert(String name, int age) { SQLiteDatabase db = openHelper.getWritableDatabase(); if(db.isOpen()) { // 当前数据库是打开的// Object数据中的数据是替换前面sql语句中的?号占位符的. db.execSQL(" insert into person(name, age) values(?, ?)" , new Object[]{name, age}); db.close(); // 数据库关闭 } }public void delete(String name) { SQLiteDatabase db = openHelper.getWritableDatabase(); if(db.isOpen()) { // 当前数据库是打开的db.execSQL(" delete from person where name = ?" , new Object[]{name}); db.close(); // 数据库关闭 } }/** * @ param name 被修改人的姓名 * @ param newAge 修改后的年龄 */ public void update(String name, int newAge) { SQLiteDatabase db = openHelper.getWritableDatabase(); if(db.isOpen()) { // 当前数据库是打开的db.execSQL(" update person set age = ? where name = ?" , new Object[]{newAge, name}); db.close(); // 数据库关闭 } }public void query(String name) { SQLiteDatabase db = openHelper.getReadableDatabase(); if(db.isOpen()) { String sql = " select age, name from person where name = ?" ; Cursor cursor = db.rawQuery(sql, new String[]{name}); if(cursor != null & & cursor.moveToFirst()) { // 第一行有数据, 并且已经移动到第一行去了int age = cursor.getInt(0); String pName = cursor.getString(1); System.out.println(" 姓名: " + pName + " , 年龄: " + age); cursor.close(); // 把游标结果集关闭, 释放掉资源 }db.close(); } }public void queryAll() { SQLiteDatabase db = openHelper.getReadableDatabase(); if(db.isOpen()) { String sql = " select age, name from person" ; Cursor cursor = db.rawQuery(sql, null); if(cursor != null & & cursor.getCount() > 0) {while(cursor.moveToNext()) { int age = cursor.getInt(0); String pName = cursor.getString(1); System.out.println(" 姓名: " + pName + " , 年龄: " + age); } cursor.close(); } db.close(); } } }

【Android 测试数据存储与访问XML解析与生成】使用事务操作SQLite数据库
使用SQLiteDatabase的beginTransaction()方法可以开启一个事务, 程序执行到endTransaction() 方法时会检查事务的标志是否为成功, 如果程序执行到endTransaction()之前调用了setTransactionSuccessful() 方法设置事务的标志为成功则提交事务, 如果没有调用setTransactionSuccessful() 方法则回滚事务。使用例子如下: SQLiteDatabase db = ….;
db.beginTransaction(); //开始事务
try {
db.execSQL(“insert into person(name, age) values(?,?)”, new Object[]{“小朋友吧”, 4});
db.execSQL(“update person set name= ? where personid= ?”, new Object[]{“朋友”, 1});
db.setTransactionSuccessful(); //调用此方法会在执行到endTransaction() 时提交当前事务, 如果不调用此方法会回滚事务
} finally {
db.endTransaction(); //由事务的标志决定是提交事务, 还是回滚事务
}
db.close();
上面两条SQL语句在同一个事务中执行。
3.XML解析与生成
( 1) Xml文件的序列化
1.序列化 : 就是把文件读到内存里。
下面是本例子要解析的XML文件:
文件名称: itcast.xml
< ?xml version= " 1.0" encoding= " UTF-8" standalone= " yes" ?> < weather> < city id= " 1" > < name> 北京< /name> < pm> 1000000< /pm> < wind> 8< /wind> < temp> 14-21< /temp> < /city> < /weather>

例子定义了一个java bean用于存放上面解析出来的xml内容, 这个java bean为Person, 代码请见本页下面备注.
public class MainActivity extends Activity {@ Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }// 序列化 XML文件 public void createxml(View v) { // 创建好xml序列化器 XmlSerializer serializer = Xml.newSerializer(); try { //初始化fos FileOutputStream fos = this.openFileOutput(" serializer.xml" , Context.MODE_PRIVATE); // 初始化serializer serializer.setOutput(fos, " utf-8" ); // 文本的开始搞出来 serializer.startDocument(" utf-8" , true); // 文本的开始节点 serializer.startTag(null, " sms" ); //info节点 serializer.startTag(null, " info" ); //给Info添加ID serializer.attribute(null, " id" , " 1" ); //name节点 serializer.startTag(null, " name" ); //在节点内添加内容 serializer.text(" < dage" ); //name结束节点 serializer.endTag(null, " name" ); //body节点 serializer.startTag(null, " body" ); //在节点内添加内容 serializer.text(" 正直的少年好儿郎" ); //body结束节点 serializer.endTag(null, " body" ); //phone节点 serializer.startTag(null, " phone" ); //在节点内添加内容 serializer.text(" 18610982665" ); //phone结束节点 serializer.endTag(null, " phone" ); //info结束节点 serializer.endTag(null, " info" ); // 文本的结束节点 serializer.endTag(null, " sms" ); // 文本的结束serializer.endDocument(); fos.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }}}

(2)Pull解析Xml文件
public class WeatherInfo {private String id; private String wind; private String pm; private String temp; private String name; public String getId() { return id; }public void setId(String id) { this.id = id; }public String getWind() { return wind; }public void setWind(String wind) { this.wind = wind; }public String getPm() { return pm; }public void setPm(String pm) { this.pm = pm; }public String getTemp() { return temp; }public void setTemp(String temp) { this.temp = temp; }public String getName() { return name; }public void setName(String name) { this.name = name; }}

public class Parser {public static WeatherInfo getPullParser(InputStream is) {// 读取xml文件时, 实例化pullparser XmlPullParser parser = Xml.newPullParser(); // 实体类, 搞出来 WeatherInfo weatherinfo = null; try { // 初始化parser parser.setInput(is, " utf-8" ); // 获取事件的类型 int type = parser.getEventType(); // 如果指针不指向文本的结尾, 就循环while (type != XmlPullParser.END_DOCUMENT) {// 如果类型是开始节点, 做一个判断了 if (type = = XmlPullParser.START_TAG) {// 事件的名称如果和我想要的这个节点名一样。 // 把里面的内容解析出来 // 解析到了weather节点下。 if (" weather" .equals(parser.getName())) {// 初始化实体类 weatherinfo = new WeatherInfo(); // 如果解析到city节点下 } else if (" city" .equals(parser.getName())) { // 获取节点下的属性 String id = parser.getAttributeValue(0); // 用实体类把值添加进去 weatherinfo.setId(id); // 如果解析到name节点 } else if (" name" .equals(parser.getName())) { // 获取name节点下一个文本 String name = parser.nextText(); // 把获取到的内容添加到实体类中 weatherinfo.setName(name); } else if (" pm" .equals(parser.getName())) { String pm = parser.nextText(); weatherinfo.setPm(pm); } else if (" wind" .equals(parser.getName())) { String wind = parser.nextText(); weatherinfo.setWind(wind); } else if (" temp" .equals(parser.getName())) { String temp = parser.nextText(); weatherinfo.setTemp(temp); }}type = parser.next(); }} catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } return weatherinfo; }}

public class MainActivity extends Activity {@ Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }// 解析文件的点击事件 public void parse(View v) { //通过类加载器的方式, 获取到输入流InputStream is = MainActivity.class.getClassLoader().getResourceAsStream(" weathers.xml" ); //实例化出天气信息 WeatherInfo weatherInfo = Parser.getPullParser(is); //打印出天气信息 Toast.makeText(this, " 城市ID:" + weatherInfo.getId() + " 城市PM" + weatherInfo.getPm(), 1) .show(); }}


    推荐阅读