完整學習筆記之Android基礎(詳版)
Android專案的目錄結構(熟悉)
- Activity:應用被開啟時顯示的介面
- src:專案程式碼
- R.java:專案中所有資原始檔的資源id
- Android.jar:Android的jar包,匯入此包方可使用Android的api
- libs:匯入第三方jar包
- assets:存放資原始檔,比方說mp3、視訊檔案
- bin:存放編譯打包後的檔案
- res:存放資原始檔,存放在此資料夾下的所有資原始檔都會生成資源id
- drawable:存放圖片資源
- layout:存放佈局檔案,把佈局檔案通過資源id指定給activity,介面就會顯示出該佈局檔案定義的佈局
- menu:定義選單的樣式
- Strings.xml:存放字串資源,每個資源都會有一個資源id
Android的配置檔案(清單檔案)(熟悉)
指定應用的包名
package="com.itheima.helloworld"
- data/data/com.itheima.helloworld(上面程式碼指定的包名)
- 應用生成的檔案都會存放在此路徑下
Android的四大元件在使用前全部需要在清單檔案中配置
- 的配置對整個應用生效
- 的配置對該activity生效
DDMS(掌握)
- Dalvik debug monitor service
- Dalvik除錯監控服務
常用的adb指令(掌握)
Android debug bridge:安卓除錯橋
- adb start-server:啟動adb程序
- adb kill-server:殺死adb程序
- adb devices:檢視當前與開發環境連線的裝置,此命令也可以啟動adb程序
- adb install XXX.apk:往模擬器安裝apk
- adb uninstall 包名:刪除模擬器中的應用
- adb shell:進入linux命令列
- ps:檢視執行程序
- ls:檢視當前目錄下的檔案結構
- netstat -ano:檢視佔用埠的程序
電話撥號器(掌握)
功能:使用者輸入一個號碼,點選撥打按鈕,啟動系統打電話的應用把號碼撥打出去
1. 定義佈局
元件必須設定寬高,否則不能通過編譯
android:layout_width="wrap_content" android:layout_height="wrap_content"
如果要在java程式碼中操作某個元件,則元件需要設定id,這樣才能在程式碼中通過id拿到這個元件
android:id="@+id/et_phone"
2. 給按鈕設定點選偵聽
給按鈕設定偵聽
//通過id拿到按鈕物件 Button bt_call = (Button) findViewById(R.id.bt_call); //給按鈕設定點選 bt_call.setOnClickListener(new MyListener());
3. 得到使用者輸入的號碼
//得到使用者輸入的號碼,先拿到輸入框元件
EditText et_phone = (EditText) findViewById(R.id.et_phone);
String phone = et_phone.getText().toString();
4. 把號碼打出去
- Android系統中基於動作機制,來呼叫系統的應用,你告訴系統你想做什麼動作,系統就會把能做這個動作的應用給你,如果沒有這個應用,會拋異常
設定動作,通過意圖告知系統
//把號碼打出去 //先建立一個意圖物件 Intent intent = new Intent(); //設定動作,打電話 intent.setAction(Intent.ACTION_CALL); intent.setData(Uri.parse("tel:" + phone)); //把意圖告訴系統 startActivity(intent);
新增許可權
<uses-permission android:name="android.permission.CALL_PHONE"/>
點選事件的四種寫法(掌握)
第一種
定義一個MyListener實現onClickListener介面
Button bt1 = (Button) findViewById(R.id.bt1); bt1.setOnClickListener(new MyListener());
第二種
定義一個匿名內部類實現onClickListener介面
Button bt2 = (Button) findViewById(R.id.bt2); bt2.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { System.out.println("第二種"); } });
第三種
讓當前activity實現onClickListener介面
Button bt3 = (Button) findViewById(R.id.bt3); bt3.setOnClickListener(this);
第四種
給Button節點設定onClick屬性,
android:onClick="click"
然後在activity中定義跟該屬性值同名的方法
public void click(View v){ System.out.println("第四種"); }
簡訊傳送器(掌握)
功能:使用者輸入號碼和簡訊內容,點擊發送按鈕,呼叫簡訊api把簡訊傳送給指定號碼
1. 定義佈局
輸入框的提示
android:hint="請輸入號碼"
2. 完成點選事件
- 先給Button元件設定onClick屬性
-
onClick=”send” - 在Activity中定義此方法
-
public void send(View v){}
3. 獲取到使用者輸入的號碼和內容
EditText et_phone = (EditText) findViewById(R.id.et_phone);
EditText et_content = (EditText) findViewById(R.id.et_content);
String phone = et_phone.getText().toString();
String content = et_content.getText().toString();
4. 呼叫傳送簡訊的api
//呼叫傳送簡訊的api
SmsManager sm = SmsManager.getDefault();
//傳送簡訊
sm.sendTextMessage(phone, null, content, null, null);
* 新增許可權
<uses-permission android:name="android.permission.SEND_SMS"/>
* 如果簡訊過長,需要拆分
List<String> smss = sm.divideMessage(content);
常用佈局
線性佈局
- LinearLayout
指定各個節點的排列方向
android:orientation="horizontal"
設定右對齊
android:layout_gravity="right"
- 當豎直佈局時,只能左右對齊和水平居中,頂部底部對齊豎直居中無效
- 當水平佈局時,只能頂部底部對齊和豎直居中
- 使用match_parent時注意不要把其他元件頂出去
線性佈局非常重要的一個屬性:權重
android:layout_weight="1"
- 權重:按比例分配螢幕的剩餘寬度或者高度
常見佈局
相對佈局(掌握)
RelativeLayout
- 元件預設左對齊、頂部對齊
設定元件在指定元件的右邊
android:layout_toRightOf="@id/tv1"
設定在指定元件的下邊
android:layout_below="@id/tv1"
設定右對齊父元素
android:layout_alignParentRight="true"
設定與指定元件右對齊
android:layout_alignRight="@id/tv1"
線性佈局(掌握)
LinearLayout
指定各個節點的排列方向
android:orientation="horizontal"
設定右對齊
android:layout_gravity="right"
- 當豎直佈局時,只能左右對齊和水平居中,頂部底部對齊豎直居中無效
- 當水平佈局時,只能頂部底部對齊和豎直居中
- 使用match_parent時注意不要把其他元件頂出去
線性佈局非常重要的一個屬性:權重
android:layout_weight="1"
- 權重:按比例分配螢幕的剩餘寬度或者高度
幀佈局(掌握)
FrameLayout
- 預設元件都是左對齊和頂部對齊,每個元件相當於一個div
可以設定上下左右對齊,水平豎直居中,設定方式與線性佈局一樣
android:layout_gravity="bottom"
- 不能相對於其他元件佈局
表格佈局(熟悉)
TableLayout
- 每個節點是一行,它的每個子節點是一列
表格佈局中的節點可以不設定寬高,因為設定了也無效
- 根節點的子節點寬為匹配父元素,高為包裹內容
- 節點的子節點寬為包裹內容,高為包裹內容
- 以上預設屬性無法修改
根節點中可以設定以下屬性,表示讓第1列拉伸填滿螢幕寬度的剩餘空間
android:stretchColumns="1"
絕對佈局(熟悉)
AbsoluteLayout
直接指定元件的x、y座標
android:layout_x="144dp" android:layout_y="154dp"
logcat(掌握)
- 日誌資訊總共分為5個等級
- verbose
- debug
- info
- warn
- error
- 定義過濾器方便檢視
- System.out.print輸出的日誌級別是info,tag是System.out
Android提供的日誌輸出api
Log.v(TAG, "加油吧,童鞋們"); Log.d(TAG, "加油吧,童鞋們"); Log.i(TAG, "加油吧,童鞋們"); Log.w(TAG, "加油吧,童鞋們"); Log.e(TAG, "加油吧,童鞋們");
檔案讀寫操作
- Ram記憶體:執行記憶體,相當於電腦的記憶體
- Rom記憶體:內部儲存空間,相當於電腦的硬碟
- sd卡:外部儲存空間,相當於電腦的行動硬碟
在內部儲存空間中讀寫檔案(掌握)
小案例:使用者輸入賬號密碼,勾選“記住賬號密碼”,點選登入按鈕,登入的同時持久化儲存賬號和密碼
1. 定義佈局
2. 完成按鈕的點選事件
彈土司提示使用者登入成功
Toast.makeText(this, "登入成功", Toast.LENGTH_SHORT).show();
3. 拿到使用者輸入的資料
判斷使用者是否勾選儲存賬號密碼
CheckBox cb = (CheckBox) findViewById(R.id.cb); if(cb.isChecked()){ }
4. 開啟io流把檔案寫入內部儲存
直接開啟檔案輸出流寫資料
//持久化儲存資料 File file = new File("data/data/com.itheima.rwinrom/info.txt"); FileOutputStream fos = new FileOutputStream(file); fos.write((name + "##" + pass).getBytes()); fos.close();
讀取資料前先檢測檔案是否存在
if(file.exists())
讀取儲存的資料,也是直接開檔案輸入流讀取
File file = new File("data/data/com.itheima.rwinrom/info.txt"); FileInputStream fis = new FileInputStream(file); //把位元組流轉換成字元流 BufferedReader br = new BufferedReader(new InputStreamReader(fis)); String text = br.readLine(); String[] s = text.split("##");
讀取到資料之後,回顯至輸入框
et_name.setText(s[0]); et_pass.setText(s[1]);
- 應用只能在自己的包名目錄下建立檔案,不能到別人家去建立
直接複製專案
- 需要改動的地方:
- 專案名字
- 應用包名
- R檔案重新導包
使用路徑api讀寫檔案(掌握)
- getFilesDir()得到的file物件的路徑是data/data/com.itheima.rwinrom2/files
- 存放在這個路徑下的檔案,只要你不刪,它就一直在
getCacheDir()得到的file物件的路徑是data/data/com.itheima.rwinrom2/cache
- 存放在這個路徑下的檔案,當記憶體不足時,有可能被刪除
系統管理應用介面的清除快取,會清除cache資料夾下的東西,清除資料,會清除整個包名目錄下的東西
在外部儲存讀寫資料
sd卡的路徑(掌握)
- 2.2之前,sd卡路徑:sdcard
- 4.3之前,sd卡路徑:mnt/sdcard
4.3開始,sd卡路徑:storage/sdcard
最簡單的開啟sd卡的方式
File file = new File("sdcard/info.txt");
寫sd卡需要許可權
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
讀sd卡,在4.0之前不需要許可權,4.0之後可以設定為需要
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
使用api獲得sd卡的真實路徑,部分手機品牌會更改sd卡的路徑
Environment.getExternalStorageDirectory()
判斷sd卡是否準備就緒
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
檢視原始碼查詢獲取sd卡剩餘容量的程式碼(掌握)
- 匯入Settings專案
查詢“可用空間”得到
<string name="memory_available" msgid="418542433817289474">"可用空間"</string>
查詢”memory_available”,得到
<Preference android:key="memory_sd_avail" style="?android:attr/preferenceInformationStyle" android:title="@string/memory_available" android:summary="00"/>
查詢”memory_sd_avail”,得到
//這個字串就是sd卡剩餘容量 formatSize(availableBlocks * blockSize) + readOnly //這兩個引數相乘,得到sd卡以位元組為單位的剩餘容量 availableBlocks * blockSize
儲存裝置會被分為若干個區塊,每個區塊有固定的大小
- 區塊大小 * 區塊數量 等於 儲存裝置的總大小
Linux檔案的訪問許可權(掌握)
- 在Android中,每一個應用是一個獨立的使用者
- drwxrwxrwx
- 第1位:d表示資料夾,-表示檔案
- 第2-4位:rwx,表示這個檔案的擁有者使用者(owner)對該檔案的許可權
- r:讀
- w:寫
- x:執行
- 第5-7位:rwx,表示跟檔案擁有者使用者同組的使用者(grouper)對該檔案的許可權
- 第8-10位:rwx,表示其他使用者組的使用者(other)對該檔案的許可權
openFileOutput的四種模式(熟悉)
- MODE_PRIVATE:-rw-rw—-
- MODE_APPEND:-rw-rw—-
- MODE_WORLD_WRITEABLE:-rw-rw–w-
- MODE_WORLD_READABLE:-rw-rw-r–
SharedPreference(掌握)
用SharedPreference儲存賬號密碼
往SharedPreference裡寫資料
//拿到一個SharedPreference物件 SharedPreferences sp = getSharedPreferences("config", MODE_PRIVATE); //拿到編輯器 Editor ed = sp.edit(); //寫資料 ed.putBoolean("name", name); ed.commit();
從SharedPreference裡取資料
SharedPreferences sp = getSharedPreferences("config", MODE_PRIVATE); //從SharedPreference裡取資料 String name = sp.getBoolean("name", "");
生成XML檔案備份簡訊
- 建立幾個虛擬的簡訊物件,存在list中
- 備份資料通常都是備份至sd卡
使用StringBuffer拼接字串(瞭解)
把整個xml檔案所有節點append到sb物件裡
sb.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"); //新增smss的開始節點 sb.append("<smss>"); .......
把sb寫到輸出流中
fos.write(sb.toString().getBytes());
使用XMl序列化器生成xml檔案(掌握)
得到xml序列化器物件
XmlSerializer xs = Xml.newSerializer();
給序列化器設定輸出流
File file = new File(Environment.getExternalStorageDirectory(), "backupsms.xml"); FileOutputStream fos = new FileOutputStream(file); //給序列化器指定好輸出流 xs.setOutput(fos, "utf-8");
開始生成xml檔案
xs.startDocument("utf-8", true); xs.startTag(null, "smss"); ......
Pull解析xml檔案(掌握)
- 先自己寫一個xml檔案,存一些天氣資訊
拿到xml檔案
InputStream is = getClassLoader().getResourceAsStream("weather.xml");
拿到pull解析器
XmlPullParser xp = Xml.newPullParser();
開始解析
拿到指標所在當前節點的事件型別
int type = xp.getEventType();
事件型別主要有五種
- START_DOCUMENT:xml頭的事件型別
- END_DOCUMENT:xml尾的事件型別
- START_TAG:開始節點的事件型別
- END_TAG:結束節點的事件型別
- TEXT:文字節點的事件型別
如果獲取到的事件型別不是END_DOCUMENT,就說明解析還沒有完成,如果是,解析完成,while迴圈結束
while(type != XmlPullParser.END_DOCUMENT)
當我們解析到不同節點時,需要進行不同的操作,所以判斷一下當前節點的name
- 當解析到weather的開始節點時,new出list
- 當解析到city的開始節點時,建立city物件,建立物件是為了更方便的儲存即將解析到的文字
當解析到name開始節點時,獲取下一個節點的文字內容,temp、pm也是一樣
case XmlPullParser.START_TAG: //獲取當前節點的名字 if("weather".equals(xp.getName())){ citys = new ArrayList<City>(); } else if("city".equals(xp.getName())){ city = new City(); } else if("name".equals(xp.getName())){ //獲取當前節點的下一個節點的文字 String name = xp.nextText(); city.setName(name); } else if("temp".equals(xp.getName())){ String temp = xp.nextText(); city.setTemp(temp); } else if("pm".equals(xp.getName())){ String pm = xp.nextText(); city.setPm(pm); } break;
當解析到city的結束節點時,說明city的三個子節點已經全部解析完了,把city物件新增至list
case XmlPullParser.END_TAG: if("city".equals(xp.getName())){ citys.add(city); }
測試(瞭解)
- 黑盒測試
- 測試邏輯業務
白盒測試
- 測試邏輯方法
根據測試粒度
- 方法測試:function test
- 單元測試:unit test
- 整合測試:integration test
- 系統測試:system test
根據測試暴力程度
- 冒煙測試:smoke test
- 壓力測試:pressure test
單元測試junit(熟悉)
定義一個類繼承AndroidTestCase,在類中定義方法,即可測試該方法
在指定指令集時,targetPackage指定你要測試的應用的包名
<instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="com.itheima.junit" ></instrumentation>
定義使用的類庫
<uses-library android:name="android.test.runner"></uses-library>
斷言的作用,檢測執行結果和預期是否一致
- 如果應用出現異常,會拋給測試框架
SQLite資料庫(掌握)
- 輕量級關係型資料庫
建立資料庫需要使用的api:SQLiteOpenHelper
必須定義一個構造方法:
//arg1:資料庫檔案的名字 //arg2:遊標工廠 //arg3:資料庫版本 public MyOpenHelper(Context context, String name, CursorFactory factory, int version){ }
- 資料庫被建立時會呼叫:onCreate方法
- 資料庫升級時會呼叫:onUpgrade方法
建立資料庫
//建立OpenHelper物件
MyOpenHelper oh = new MyOpenHelper(getContext(), "person.db", null, 1);
//獲得資料庫物件,如果資料庫不存在,先建立資料庫,後獲得,如果存在,則直接獲得
SQLiteDatabase db = oh.getWritableDatabase();
- getWritableDatabase():開啟可讀寫的資料庫
- getReadableDatabase():在磁碟空間不足時開啟只讀資料庫,否則開啟可讀寫資料庫
在建立資料庫時建立表
public void onCreate(SQLiteDatabase db) { // TODO Auto-generated method stub db.execSQL("create table person (_id integer primary key autoincrement, name char(10), phone char(20), money integer(20))"); }
資料庫的增刪改查(掌握)
SQL語句
- insert into person (name, phone, money) values (‘張三’, ‘159874611’, 2000);
- delete from person where name = ‘李四’ and _id = 4;
- update person set money = 6000 where name = ‘李四’;
- select name, phone from person where name = ‘張三’;
執行SQL語句實現增刪改查
//插入
db.execSQL("insert into person (name, phone, money) values (?, ?, ?);", new Object[]{"張三", 15987461, 75000});
//查詢
Cursor cs = db.rawQuery("select _id, name, money from person where name = ?;", new String[]{"張三"});
* 測試方法執行前會呼叫此方法
protected void setUp() throws Exception {
super.setUp();
// 獲取虛擬上下文物件
oh = new MyOpenHelper(getContext(), "people.db", null, 1);
}
* 測試方法執行後會呼叫此方法:tearDown()
使用api實現增刪改查
插入
//以鍵值對的形式儲存要存入資料庫的資料 ContentValues cv = new ContentValues(); cv.put("name", "劉能"); cv.put("phone", 1651646); cv.put("money", 3500); //返回值是改行的主鍵,如果出錯返回-1 long i = db.insert("person", null, cv);
刪除
//返回值是刪除的行數 int i = db.delete("person", "_id = ? and name = ?", new String[]{"1", "張三"});
修改
ContentValues cv = new ContentValues(); cv.put("money", 25000); int i = db.update("person", cv, "name = ?", new String[]{"趙四"});
查詢
//arg1:要查詢的欄位 //arg2:查詢條件 //arg3:填充查詢條件的佔位符 Cursor cs = db.query("person", new String[]{"name", "money"}, "name = ?", new String[]{"張三"}, null, null, null); while(cs.moveToNext()){ // 獲取指定列的索引值 String name = cs.getString(cs.getColumnIndex("name")); String money = cs.getString(cs.getColumnIndex("money")); System.out.println(name + ";" + money); }
事務
- 保證多條SQL語句要麼同時成功,要麼同時失敗
- 最常見案例:銀行轉賬
事務api
try { //開啟事務 db.beginTransaction(); ........... //設定事務執行成功 db.setTransactionSuccessful(); } finally{ //關閉事務 //如果此時已經設定事務執行成功,則sql語句生效,否則不生效 db.endTransaction(); }
把資料庫的資料顯示至螢幕(瞭解)
任意插入一些資料
- 定義業務bean:Person.java
讀取資料庫的所有資料
Cursor cs = db.query(“person”, null, null, null, null, null, null);
while(cs.moveToNext()){
String name = cs.getString(cs.getColumnIndex(“name”));
String phone = cs.getString(cs.getColumnIndex(“phone”));
String money = cs.getString(cs.getColumnIndex(“money”));
//把讀到的資料封裝至Person物件
Person p = new Person(name, phone, money);
//把person物件儲存至集合中
people.add(p);
}把集合中的資料顯示至螢幕
LinearLayout ll = (LinearLayout) findViewById(R.id.ll);
for(Person p : people){
//建立TextView,每條資料用一個文字框顯示
TextView tv = new TextView(this);
tv.setText(p.toString());
//把文字框設定為ll的子節點
ll.addView(tv);
}分頁查詢
Cursor cs = db.query(“person”, null, null, null, null, null, null, “0, 10”);
ListView(掌握)
- 用於顯示列表
- MVC結構
- M:model模型層,要顯示的資料 ————people集合
- V:view檢視層,使用者看到的介面 ————ListView
- c:control控制層,操作資料如何顯示 ————adapter物件
- 每一個條目都是一個View物件
BaseAdapter
必須實現的兩個方法
第一個
//系統呼叫此方法,用來獲知模型層有多少條資料 @Override public int getCount() { return people.size(); }
第二個
//系統呼叫此方法,獲取要顯示至ListView的View物件 //position:是return的View物件所對應的資料在集合中的位置 @Override public View getView(int position, View convertView, ViewGroup parent) { System.out.println("getView方法呼叫" + position); TextView tv = new TextView(MainActivity.this); //拿到集合中的元素 Person p = people.get(position); tv.setText(p.toString()); //把TextView的物件返回出去,它會變成ListView的條目 return tv; }
- 螢幕上能顯示多少個條目,getView方法就會被呼叫多少次,螢幕向下滑動時,getView會繼續被呼叫,建立更多的View物件顯示至螢幕
條目的快取
- 當條目劃出螢幕時,系統會把該條目快取至記憶體,當該條目再次進入螢幕,系統在重新呼叫getView時會把快取的條目作為convertView引數傳入,但是傳入的條目不一定是之前被快取的該條目,即系統有可能在呼叫getView方法獲取第一個條目時,傳入任意一個條目的快取
ArrayAdapter(熟悉)
在條目中顯示一個字串
String[] objects = new String[]{ "張三", "李四", "王五" }; ListView lv = (ListView) findViewById(R.id.lv); //arg1:指定要填充的佈局檔案 //arg2:指定文字顯示至哪一個文字框內 lv.setAdapter(new ArrayAdapter<String>(this, R.layout.item_array, R.id.tv_name, objects));
SimpleAdapter(熟悉)
- 可在條目中顯示多種資料
要顯示的資料封裝在List中,集合的每一個元素存放的是一個條目會顯示的資料,因為可能會有多種資料,而集合的泛型只能指定一種資料,所以把資料先存放在Map中,在把Map放入List中
List<Map<String, Object>> data = new ArrayList<Map<String,Object>>(); //張三的頭像和名字是兩種型別的資料,先封裝至Map Map<String, Object> map1 = new HashMap<String, Object>(); map1.put("name", "張三"); map1.put("image", R.drawable.photo1); //把Map封裝至List data.add(map1); ...
通過兩個陣列的下標對應指定資料存放入對應的控制元件中
lv.setAdapter(new SimpleAdapter(this, data, R.layout.item_array,
new String[]{“name”, “image”}, new int[]{R.id.tv_name, R.id.iv_photo}));
網路圖片檢視器(掌握)
- 確定圖片的網址
傳送http請求
//1.使用網址構造一個URL物件
URL url = new URL(address);
//2.獲取連線物件,並沒有建立連線
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//3.設定一些屬性
//設定連線和讀取超時
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
//設定請求方法,注意必須大寫
conn.setRequestMethod(“GET”);
//建立連線,傳送get請求
//conn.connect();
//建立連線,然後獲取響應嗎,200說明請求成功
conn.getResponseCode();伺服器的圖片是以流的形式返回給瀏覽器的
//拿到伺服器返回的輸入流
InputStream is = conn.getInputStream();
//把流裡的資料讀取出來,並構造成圖片
Bitmap bm = BitmapFactory.decodeStream(is);把圖片設定為ImageView的顯示內容
ImageView iv = (ImageView) findViewById(R.id.iv); iv.setImageBitmap(bm);
- 新增許可權
主執行緒不能被阻塞
- 在Android中,主執行緒被阻塞會導致應用不能重新整理ui介面,不能響應使用者操作,使用者體驗將非常差
- 主執行緒阻塞時間過長,系統會丟擲ANR異常
- ANR:Application Not Response;應用無響應
- 任何耗時操作都不可以寫在主執行緒
- 因為網路互動屬於耗時操作,如果網速很慢,程式碼會阻塞,所以網路互動的程式碼不能執行在主執行緒
只有主執行緒能重新整理ui
- 重新整理ui的程式碼只能執行在主執行緒,執行在子執行緒是沒有任何效果的
- 如果需要在子執行緒中重新整理ui,使用訊息佇列機制
- 主執行緒也叫ui執行緒
訊息佇列(重點掌握)
- 主執行緒建立時,系統會同時建立訊息佇列物件(MessageQueue)和訊息輪詢器物件(Looper)
- 輪詢器的作用,就是不停的檢測訊息佇列中是否有訊息(Message)
- Looper一旦發現Message Queue中有訊息,就會把訊息取出,然後把訊息扔給Handler物件,Handler會呼叫自己的handleMessage方法來處理這條訊息
- handleMessage方法執行在主執行緒
主執行緒建立時,訊息佇列和輪詢器物件就會被建立,但是訊息處理器物件,需要使用時,自行建立
//訊息佇列 Handler handler = new Handler(){ //主執行緒中有一個訊息輪詢器looper,不斷檢測訊息佇列中是否有新訊息,如果發現有新訊息,自動呼叫此方法,注意此方法是在主執行緒中執行的 public void handleMessage(android.os.Message msg) { } };
在子執行緒中使用Handler物件往訊息佇列裡發訊息
//建立訊息物件 Message msg = new Message(); //訊息的obj屬性可以賦值任何物件,通過這個屬性可以攜帶資料 msg.obj = bm; //what屬性相當於一個標籤,用於區分出不同的訊息,從而執行不能的程式碼 msg.what = 1; //傳送訊息 handler.sendMessage(msg);
通過switch語句區分不同的訊息
public void handleMessage(android.os.Message msg) { switch (msg.what) { //如果是1,說明屬於請求成功的訊息 case 1: ImageView iv = (ImageView) findViewById(R.id.iv); Bitmap bm = (Bitmap) msg.obj; iv.setImageBitmap(bm); break; case 2: Toast.makeText(MainActivity.this, "請求失敗", 0).show(); break; } }
加入快取圖片的功能(熟悉)
把伺服器返回的流裡的資料讀取出來,然後通過檔案輸入流寫至本地檔案
//1.拿到伺服器返回的輸入流 InputStream is = conn.getInputStream(); //2.把流裡的資料讀取出來,並構造成圖片 FileOutputStream fos = new FileOutputStream(file); byte[] b = new byte[1024]; int len = 0; while((len = is.read(b)) != -1){ fos.write(b, 0, len); }
建立bitmap物件的程式碼改成
Bitmap bm = BitmapFactory.decodeFile(file.getAbsolutePath());
- 每次傳送請求前檢測一下在快取中是否存在同名圖片,如果存在,則讀取快取
獲取開原始碼的網站(熟悉)
- code.google.com
- github.com
- 在github搜尋smart-image-view
- 下載開源專案smart-image-view
使用自定義元件時,標籤名字要寫包名
<com.loopj.android.image.SmartImageView/>
SmartImageView的使用
SmartImageView siv = (SmartImageView) findViewById(R.id.siv); siv.setImageUrl("http://192.168.1.102:8080/dd.jpg");
Html原始檔檢視器(掌握)
傳送GET請求
URL url = new URL(path); //獲取連線物件 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //設定連線屬性 conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); //建立連線,獲取響應嗎 if(conn.getResponseCode() == 200){ }
獲取伺服器返回的流,從流中把html原始碼讀取出來
byte[] b = new byte[1024]; int len = 0; ByteArrayOutputStream bos = new ByteArrayOutputStream(); while((len = is.read(b)) != -1){ //把讀到的位元組先寫入位元組陣列輸出流中存起來 bos.write(b, 0, len); } //把位元組陣列輸出流中的內容轉換成字串 //預設使用utf-8 text = new String(bos.toByteArray());
亂碼的處理
亂碼的出現是因為伺服器和客戶端碼錶不一致導致
//手動指定碼錶 text = new String(bos.toByteArray(), "gb2312");
提交資料(掌握)
GET方式提交資料
get方式提交的資料是直接拼接在url的末尾
final String path = "http://192.168.1.104/Web/servlet/CheckLogin?name=" + name + "&pass=" + pass;
傳送get請求,程式碼和之前一樣
URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setReadTimeout(5000); conn.setConnectTimeout(5000); if(conn.getResponseCode() == 200){ }
瀏覽器在傳送請求攜帶資料時會對資料進行URL編碼,我們寫程式碼時也需要為中文進行URL編碼
String path = "http://192.168.1.104/Web/servlet/CheckLogin?name=" + URLEncoder.encode(name) + "&pass=" + pass;
POST方式提交資料
- post提交資料是用輸出流寫給伺服器的
協議頭中多了兩個屬性
- Content-Type: application/x-www-form-urlencoded,描述提交的資料的mimetype
Content-Length: 32,描述提交的資料的長度
//給請求頭新增post多出來的兩個屬性 String data = "name=" + URLEncoder.encode(name) + "&pass=" + pass; conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("Content-Length", data.length() + "");
設定允許開啟post請求的流
conn.setDoOutput(true);
獲取連線物件的輸出流,往流裡寫要提交給伺服器的資料
OutputStream os = conn.getOutputStream(); os.write(data.getBytes());
HttpClient(掌握)
傳送get請求
建立一個客戶端物件
HttpClient client = new DefaultHttpClient();
建立一個get請求物件
HttpGet hg = new HttpGet(path);
傳送get請求,建立連線,返回響應頭物件
HttpResponse hr = client.execute(hg);
獲取狀態行物件,獲取狀態碼,如果為200則說明請求成功
if(hr.getStatusLine().getStatusCode() == 200){ //拿到伺服器返回的輸入流 InputStream is = hr.getEntity().getContent(); String text = Utils.getTextFromStream(is); }
傳送post請求
//建立一個客戶端物件
HttpClient client = new DefaultHttpClient();
//建立一個post請求物件
HttpPost hp = new HttpPost(path);
往post物件裡放入要提交給伺服器的資料
//要提交的資料以鍵值對的形式存在BasicNameValuePair物件中 List<NameValuePair> parameters = new ArrayList<NameValuePair>(); BasicNameValuePair bnvp = new BasicNameValuePair("name", name); BasicNameValuePair bnvp2 = new BasicNameValuePair("pass", pass); parameters.add(bnvp); parameters.add(bnvp2); //建立實體物件,指定進行URL編碼的碼錶 UrlEncodedFormEntity entity = new UrlEncodedFormEntity(parameters, "utf-8"); //為post請求設定實體 hp.setEntity(entity);
非同步HttpClient框架(熟悉)
傳送get請求
//建立非同步的httpclient物件
AsyncHttpClient ahc = new AsyncHttpClient();
//傳送get請求
ahc.get(path, new MyHandler());
* 注意AsyncHttpResponseHandler兩個方法的呼叫時機
class MyHandler extends AsyncHttpResponseHandler{
//http請求成功,返回碼為200,系統回撥此方法
@Override
public void onSuccess(int statusCode, Header[] headers,
//responseBody的內容就是伺服器返回的資料
byte[] responseBody) {
Toast.makeText(MainActivity.this, new String(responseBody), 0).show();
}
//http請求失敗,返回碼不為200,系統回撥此方法
@Override
public void onFailure(int statusCode, Header[] headers,
byte[] responseBody, Throwable error) {
Toast.makeText(MainActivity.this, "返回碼不為200", 0).show();
}
}
傳送post請求
使用RequestParams物件封裝要攜帶的資料
//建立非同步httpclient物件 AsyncHttpClient ahc = new AsyncHttpClient(); //建立RequestParams封裝要攜帶的資料 RequestParams rp = new RequestParams(); rp.add("name", name); rp.add("pass", pass); //傳送post請求 ahc.post(path, rp, new MyHandler());
多執行緒下載(掌握)
原理:伺服器CPU分配給每條執行緒的時間片相同,伺服器頻寬平均分配給每條執行緒,所以客戶端開啟的執行緒越多,就能搶佔到更多的伺服器資源
確定每條執行緒下載多少資料(掌握)
傳送http請求至下載地址
String path = "http://192.168.1.102:8080/editplus.exe"; URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(5000); conn.setConnectTimeout(5000); conn.setRequestMethod("GET");
獲取檔案總長度,然後建立長度一致的臨時檔案
if(conn.getResponseCode() == 200){ //獲得伺服器流中資料的長度 int length = conn.getContentLength(); //建立一個臨時檔案儲存下載的資料 RandomAccessFile raf = new RandomAccessFile(getFileName(path), "rwd"); //設定臨時檔案的大小 raf.setLength(length); raf.close();
確定執行緒下載多少資料
//計算每個執行緒下載多少資料 int blockSize = length / THREAD_COUNT;
計算每條執行緒下載資料的開始位置和結束位置(掌握)
for(int id = 1; id <= 3; id++){
//計算每個執行緒下載資料的開始位置和結束位置
int startIndex = (id - 1) * blockSize;
int endIndex = id * blockSize - 1;
if(id == THREAD_COUNT){
endIndex = length;
}
//開啟執行緒,按照計算出來的開始結束位置開始下載資料
new DownLoadThread(startIndex, endIndex, id).start();
}
再次傳送請求至下載地址,請求開始位置至結束位置的資料(掌握)
String path = "http://192.168.1.102:8080/editplus.exe";
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(5000);
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
//向伺服器請求部分資料
conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
conn.connect();
* 下載請求到的資料,存放至臨時檔案中
if(conn.getResponseCode() == 206){
InputStream is = conn.getInputStream();
RandomAccessFile raf = new RandomAccessFile(getFileName(path), "rwd");
//指定從哪個位置開始存放資料
raf.seek(startIndex);
byte[] b = new byte[1024];
int len;
while((len = is.read(b)) != -1){
raf.write(b, 0, len);
}
raf.close();
}
帶斷點續傳的多執行緒下載(掌握)
定義一個int變數記錄每條執行緒下載的資料總長度,然後加上該執行緒的下載開始位置,得到的結果就是下次下載時,該執行緒的開始位置,把得到的結果存入快取檔案
//用來記錄當前執行緒總的下載長度 int total = 0; while((len = is.read(b)) != -1){ raf.write(b, 0, len); total += len; //每次下載都把新的下載位置寫入快取文字檔案 RandomAccessFile raf2 = new RandomAccessFile(threadId + ".txt", "rwd"); raf2.write((startIndex + total + "").getBytes()); raf2.close(); }
下次下載開始時,先讀取快取檔案中的值,得到的值就是該執行緒新的開始位置
FileInputStream fis = new FileInputStream(file); BufferedReader br = new BufferedReader(new InputStreamReader(fis)); String text = br.readLine(); int newStartIndex = Integer.parseInt(text); //把讀到的值作為新的開始位置 startIndex = newStartIndex; fis.close();
三條執行緒都下載完畢之後,刪除快取檔案
RUNNING_THREAD--; if(RUNNING_THREAD == 0){ for(int i = 0; i <= 3; i++){ File f = new File(i + ".txt"); f.delete(); } }
手機版的斷點續傳多執行緒下載器
- 把剛才的程式碼直接貼上過來就能用,記得在訪問檔案時的路徑要改成Android的目錄,新增訪問網路和外部儲存的路徑
用進度條顯示下載進度(掌握)
拿到下載檔案總長度時,設定進度條的最大值
//設定進度條的最大值 pb.setMax(length);
進度條需要顯示三條執行緒的整體下載進度,所以三條執行緒每下載一次,就要把新下載的長度加入進度條
定義一個int全域性變數,記錄三條執行緒的總下載長度
int progress;
重新整理進度條
while((len = is.read(b)) != -1){ raf.write(b, 0, len); //把當前執行緒本次下載的長度加到進度條裡 progress += len; pb.setProgress(progress);
每次斷點下載時,從新的開始位置開始下載,進度條也要從新的位置開始顯示,在讀取快取檔案獲取新的下載開始位置時,也要處理進度條進度
FileInputStream fis = new FileInputStream(file); BufferedReader br = new BufferedReader(new InputStreamReader(fis)); String text = br.readLine(); int newStartIndex = Integer.parseInt(text); //新開始位置減去原本的開始位置,得到已經下載的資料長度 int alreadyDownload = newStartIndex - startIndex; //把已經下載的長度設定入進度條 progress += alreadyDownload;
新增文字框顯示百分比進度(熟悉)
tv.setText(progress * 100 / pb.getMax() + "%");
HttpUtils框架(github上面的開原始碼)的使用(熟悉)
HttpUtils本身就支援多執行緒斷點續傳,使用起來非常的方便
* 建立HttpUtils物件
HttpUtils http = new HttpUtils();
* 下載檔案
http.download(url, //下載請求的網址
target, //下載的資料儲存路徑和檔名
true, //是否開啟斷點續傳
true, //如果伺服器響應頭中包含了檔名,那麼下載完畢後自動重新命名
new RequestCallBack<File>() {//偵聽下載狀態
//下載成功此方法呼叫
@Override
public void onSuccess(ResponseInfo<File> arg0) {
tv.setText("下載成功" + arg0.result.getPath());
}
//下載失敗此方法呼叫,比如檔案已經下載、沒有網路許可權、檔案訪問不到,方法傳入一個字串引數告知失敗原因
@Override
public void onFailure(HttpException arg0, String arg1) {
tv.setText("下載失敗" + arg1);
}
//在下載過程中不斷的呼叫,用於重新整理進度條
@Override
public void onLoading(long total, long current, boolean isUploading) {
super.onLoading(total, current, isUploading);
//設定進度條總長度
pb.setMax((int) total);
//設定進度條當前進度
pb.setProgress((int) current);
tv_progress.setText(current * 100 / total + "%");
}
});
建立第二個Activity(掌握)
- 需要在清單檔案中為其配置一個activity標籤
標籤中如果帶有這個子節點,則會在系統中多建立一個快捷圖示
<intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter>
- 一個應用程式可以在桌面建立多個快捷圖示。
activity的名稱、圖示可以和應用程式的名稱、圖示不相同
android:icon="@drawable/ic_launcher" android:label="@string/app_name"
Activity的跳轉(掌握)
Activity的跳轉需要建立Intent物件,通過設定intent物件的引數指定要跳轉Activity
通過設定Activity的包名和類名實現跳轉,稱為顯式意圖
通過指定動作實現跳轉,稱為隱式意圖
顯式意圖(掌握)
跳轉至同一專案下的另一個Activity,直接指定該Activity的位元組碼即可
Intent intent = new Intent(); intent.setClass(this, SecondActivity.class); startActivity(intent);
跳轉至其他應用中的Activity,需要指定該應用的包名和該Activity的類名
Intent intent = new Intent(); //啟動系統自帶的撥號器應用 intent.setClassName("com.android.dialer", "com.android.dialer.DialtactsActivity"); startActivity(intent);
隱式意圖(掌握)
隱式意圖跳轉至指定Activity
Intent intent = new Intent(); //啟動系統自帶的撥號器應用 intent.setAction(Intent.ACTION_DIAL); startActivity(intent);
要讓一個Activity可以被隱式啟動,需要在清單檔案的activity節點中設定intent-filter子節點
<intent-filter > <action android:name="com.itheima.second"/> <data android:scheme="asd" android:mimeType="aa/bb"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter>
- action 指定動作(可以自定義,可以使用系統自帶的)
- data 指定資料(操作什麼內容)
- category 類別 (預設類別,機頂盒,車載電腦)
- 隱式意圖啟動Activity,需要為intent設定以上三個屬性,且值必須與該Activity在清單檔案中對三個屬性的定義匹配
- intent-filter節點及其子節點都可以同時定義多個,隱式啟動時只需與任意一個匹配即可
獲取通過setData傳遞的資料(掌握)
//獲取啟動此Activity的intent物件
Intent intent = getIntent();
Uri uri = intent.getData();
顯式意圖和隱式意圖的應用場景(掌握)
- 顯式意圖用於啟動同一應用中的Activity
- 隱式意圖用於啟動不同應用中的Activity
- 如果系統中存在多個Activity的intent-filter同時與你的intent匹配,那麼系統會顯示一個對話方塊,列出所有匹配的Activity,由使用者選擇啟動哪一個
Activity跳轉時的資料傳遞(掌握)
Activity通過Intent啟動時,可以通過Intent物件攜帶資料到目標Activity
Intent intent = new Intent(this, SecondActivity.class); intent.putExtra("maleName", maleName); intent.putExtra("femaleName", femaleName); startActivity(intent);
在目標Activity中取出資料
Intent intent = getIntent(); String maleName = intent.getStringExtra("maleName"); String femaleName = intent.getStringExtra("femaleName");
Activity生命週期(掌握)
void onCreate()
- Activity已經被建立完畢
void onStart()
- Activity已經顯示在螢幕,但沒有得到焦點
void onResume()
- Activity得到焦點,可以與使用者互動
void onPause()
- Activity失去焦點,無法再與使用者互動,但依然可見
void onStop()
- Activity不可見,進入後臺
void onDestroy()
- Activity被銷燬
void onRestart()
- Activity從不可見變成可見時會執行此方法
使用場景
- Activity建立時需要初始化資源,銷燬時需要釋放資源;或者播放器應用,在介面進入後臺時需要自動暫停
完整生命週期(entire lifetime)
onCreate–>onStart–>onResume–>onPause–>onStop–>onDestory
可視生命週期(visible lifetime)
onStart–>onResume–>onPause–>onStop
前臺生命週期(foreground lifetime)
onResume–>onPause
Activity的四種啟動模式(掌握)
每個應用會有一個Activity任務棧,存放已啟動的Activity
Activity的啟動模式,修改任務棧的排列情況
- standard 標準啟動模式
- singleTop 單一頂部模式
- 如果任務棧的棧頂存在這個要開啟的activity,不會重新的建立activity,而是複用已經存在的activity。保證棧頂如果存在,不會重複建立。
- 應用場景:瀏覽器的書籤
singeTask 單一任務棧,在當前任務棧裡面只能有一個例項存在
- 當開啟activity的時候,就去檢查在任務棧裡面是否有例項已經存在,如果有例項存在就複用這個已經存在的activity,並且把這個activity上面的所有的別的activity都清空,複用這個已經存在的activity。保證整個任務棧裡面只有一個例項存在
- 應用場景:瀏覽器的activity
- 如果一個activity的建立需要佔用大量的系統資源(cpu,記憶體)一般配置這個activity為singletask的啟動模式。webkit核心 c程式碼
singleInstance啟動模式非常特殊, activity會執行在自己的任務棧裡面,並且這個任務棧裡面只有一個例項存在
- 如果你要保證一個activity在整個手機作業系統裡面只有一個例項存在,使用singleInstance
- 應用場景: 電話撥打介面
橫豎屏切換的生命週期(熟悉)
預設情況下 ,橫豎屏切換, 銷燬當前的activity,重新建立一個新的activity
快捷鍵ctrl+F11
在一些特殊的應用程式常見下,比如遊戲,不希望橫豎屏切換activity被銷燬重新建立
需求:禁用掉橫豎屏切換的生命週期
橫豎屏寫死
android:screenOrientation=”landscape”
android:screenOrientation=”portrait”讓系統的環境 不再去敏感橫豎屏的切換。
android:configChanges="orientation|screenSize|keyboardHidden"
掌握開啟activity獲取返回值(掌握)
從A介面開啟B介面, B介面關閉的時候,返回一個數據給A介面
步驟:
開啟activity並且獲取返回值
startActivityForResult(intent, 0);
在新開啟的介面裡面實現設定資料的邏輯
Intent data = new Intent(); data.putExtra("phone", phone); //設定一個結果資料,資料會返回給呼叫者 setResult(0, data); finish();//關閉掉當前的activity,才會返回資料
在開啟者activity裡面實現方法
//通過data獲取返回的資料 onActivityResult(int requestCode, int resultCode, Intent data) { }
- 通過判斷請求碼和結果碼確定返回值的作用
廣播(掌握)
- 廣播的概念
- 現實:電臺通過傳送廣播發布訊息,買個收音機,就能收聽
- Android:系統在產生某個事件時傳送廣播,應用程式使用廣播接收者接收這個廣播,就知道系統產生了什麼事件。
Android系統在執行的過程中,會產生很多事件,比如開機、電量改變、收發簡訊、撥打電話、螢幕解鎖
廣播接收者(掌握)
- 當一條廣播被髮送出來時,系統是在所有清單檔案中遍歷,通過匹配意圖過濾器找到能接收這條廣播的廣播接收者
IP撥號器(掌握)
原理:接收撥打電話的廣播,修改廣播內攜帶的電話號碼
* 定義廣播接收者接收打電話廣播
public class CallReceiver extends BroadcastReceiver {
//當廣播接收者接收到廣播時,此方法會呼叫
@Override
public void onReceive(Context context, Intent intent) {
//拿到使用者撥打的號碼
String number = getResultData();
//修改廣播內的號碼
setResultData("17951" + number);
}
}
* 在清單檔案中定義該廣播接收者接收的廣播型別
<receiver android:name="com.itheima.ipdialer.CallReceiver">
<intent-filter >
<action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
</intent-filter>
</receiver>
* 接收打電話廣播需要許可權
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
* 即使廣播接收者的程序沒有啟動,當系統傳送的廣播可以被該接收者接收時,系統會自動啟動該接收者所在的程序
簡訊攔截器(熟悉)
系統收到簡訊時會產生一條廣播,廣播中包含了簡訊的號碼和內容
定義廣播接收者接收簡訊廣播
public void onReceive(Context context, Intent intent) { //拿到廣播裡攜帶的簡訊內容 Bundle bundle = intent.getExtras(); Object[] objects = (Object[]) bundle.get("pdus"); for(Object ob : objects ){ //通過object物件建立一個簡訊物件 SmsMessage sms = SmsMessage.createFromPdu((byte[])ob); System.out.println(sms.getMessageBody()); System.out.println(sms.getOriginatingAddress()); }
}
- 系統建立廣播時,把簡訊存放到一個數組,然後把資料以pdus為key存入bundle,再把bundle存入intent
清單檔案中配置廣播接收者接收的廣播型別,注意要設定優先順序屬性,要保證優先順序高於簡訊應用,才可以實現攔截
<receiver android:name="com.itheima.smslistener.SmsReceiver"> <intent-filter android:priority="1000"> <action android:name="android.provider.Telephony.SMS_RECEIVED"/> </intent-filter> </receiver>
新增許可權
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
4.0之後,廣播接收者所在的應用必須啟動過一次,才能生效
- 4.0之後,如果廣播接收者所在應用被使用者手動關閉了,那麼再也不會啟動了,直到使用者再次手動啟動該應用
監聽SD卡狀態(掌握)
清單檔案中定義廣播接收者接收的型別,監聽SD卡常見的三種狀態,所以廣播接收者需要接收三種廣播
<receiver android:name="com.itheima.sdcradlistener.SDCardReceiver"> <intent-filter > <action android:name="android.intent.action.MEDIA_MOUNTED"/> <action android:name="android.intent.action.MEDIA_UNMOUNTED"/> <action android:name="android.intent.action.MEDIA_REMOVED"/> <data android:scheme="file"/> </intent-filter> </receiver>
廣播接收者的定義
public class SDCardReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // 區分接收到的是哪個廣播 String action = intent.getAction(); if(action.equals("android.intent.action.MEDIA_MOUNTED")){ System.out.println("sd卡就緒"); } else if(action.equals("android.intent.action.MEDIA_UNMOUNTED")){ System.out.println("sd卡被移除"); } else if(action.equals("android.intent.action.MEDIA_REMOVED")){ System.out.println("sd卡被拔出"); } } }
勒索軟體(掌握)
- 接收開機廣播,在廣播接收者中啟動勒索的Activity
清單檔案中配置接收開機廣播
<receiver android:name="com.itheima.lesuo.BootReceiver"> <intent-filter > <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter> </receiver>
許可權
<uses-permissi