一、基础篇 1 )UI 线程概念 Android 为单线程模型。当一个程序第一次启动时,Android会自动创建一个对应的主线程(Main Thread)。它负责把事件分派到相应的控件,用于用户与Android控件进行交互。所以通常又被叫做 UI 线程。 在开发Android应用时必须遵守单线程模型的原则: Android UI 操作并不是线程安全的并且这些操作必须在UI 线程中执行。 简单表述为:1、不要阻塞UI线程;2、只能在主线程操作UI。 详见Android帮助文档Dev Guide,左侧栏Processes and Threads。 2 )UI 线程示例 2.1)UI线程阻塞 不考虑Android单线程模型,将所有任务都在该线程中执行,尤其是某些耗时操作,会使得整个用户界面被阻塞。从用户角度来看,就是按键或触屏后响应很慢甚至毫无响应。而且当应用程序阻塞的时间过长时,Android还会向用户提示一个无响应的对话框(不截了==)。 2.2)非主线程更新UI 1、LogCat会报如下的错误消息:

Uncaught handler: thread Thread-9 exiting due to uncaught exception。

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

2、Android会向用户提示一个强行关闭的对话框(又不截了==)。 2.3)非主线程更新UI 三种方式:①Handler;②View.post(Runnable);③Activity.runOnUiThread(Runnable)。 2.4)好了,开始动手测试吧^^
        
  1. publicclassUIThreadActivityextendsBaseActivity{
  2. /**标签*/
  3. privateTextViewtextView;
  4. /**Runnable*/
  5. privateRunnablerunnable=newRunnable(){
  6. @Override
  7. publicvoidrun(){
  8. logThreadId();//打印当前线程ID
  9. textView.setText(tag+",I'mJoin!");//执行UI操作
  10. }
  11. };
  12. @Override
  13. protectedvoidonCreate(BundlesavedInstanceState){
  14. super.onCreate(savedInstanceState);
  15. setContentView(R.layout.ui_thread);
  16. tag="UIThreadActivity";
  17. textView=(TextView)findViewById(R.id.textView);
  18. }
  19. /**1.1UI线程阻塞*/
  20. publicvoidblockUi(Viewv){
  21. tag="blockUi";
  22. logThreadId();//打印当前线程ID
  23. /*读取网页内容并显示*/
  24. textView.setText("开始读取网页!");//该步可能未显示,为什么?
  25. StringBufferdocument=loadHtml("http://www.google.com");//耗时操作:读取网页源码
  26. textView.setText(null==document?"读取失败!":document.toString());//显示网页源码
  27. /**
  28. *1.观察按钮呈按下状态持续时间<br>
  29. *2.尝试在按钮呈按下状态时,进行按键和触屏操作<br>
  30. */
  31. }
  32. /**1.2非主线程更新UI*/
  33. publicvoidupdateUi(Viewv){
  34. tag="updateUi";
  35. newThread(runnable).start();
  36. }
  37. /**2.1方法1:Handler*/
  38. publicvoidmethodOne(Viewv){
  39. tag="methodOne";
  40. //Can'tcreatehandlerinsidethreadthathasnotcalled
  41. //Looper.prepare();
  42. finalHandlerhandler=newHandler();
  43. newThread(newRunnable(){
  44. @Override
  45. publicvoidrun(){
  46. logThreadId();//打印当前线程ID
  47. handler.post(runnable);
  48. //handler.postDelayed(runnable,1000);
  49. }
  50. }).start();
  51. }
  52. /**2.2方法2:View.post(Runnable)*/
  53. publicvoidmethodTwo(Viewv){
  54. tag="methodTwo";
  55. newThread(newRunnable(){
  56. @Override
  57. publicvoidrun(){
  58. logThreadId();//打印当前线程ID
  59. textView.post(runnable);
  60. //textView.postDelayed(runnable,1000);
  61. }
  62. }).start();
  63. }
  64. /**2.3方法3:Activity.runOnUiThread(Runnable)*/
  65. publicvoidmethodThree(Viewv){
  66. tag="methodThree";
  67. newThread(newRunnable(){
  68. @Override
  69. publicvoidrun(){
  70. logThreadId();//打印当前线程ID
  71. runOnUiThread(runnable);
  72. }
  73. }).start();
  74. }
  75. /**
  76. *读取网页源码
  77. */
  78. privateStringBufferloadHtml(StringurlStr){
  79. try{
  80. StringBufferdoc=newStringBuffer();
  81. URLurl=newURL(urlStr);
  82. URLConnectionconn=url.openConnection();
  83. BufferedReaderreader=newBufferedReader(newInputStreamReader(
  84. conn.getInputStream()));
  85. Stringline=null;
  86. while((line=reader.readLine())!=null)
  87. doc.append(line);
  88. reader.close();
  89. returndoc;
  90. }catch(IOExceptione){
  91. e.printStackTrace();
  92. }
  93. returnnull;
  94. }
  95. }
亲,记得看Log日志出的线程id哦。(卖下萌,不介意吧?) 二、提高篇 1 )Android 消息处理机制 熟悉Android开发的可能知道其应用程序都是由消息驱动的。参考了Windows的消息循环机制,Android系统通过Handler、Thread、Looper以及MessageQueue实现了这种消息机制。相关的类都被定义在了package android.os。 推荐这篇博客咯—— 解析Android消息处理机制。(这个,那个,还建模==) 2 )实用的消息处理方式 2.1)Thread实现消息循环
        
  1. /**1.1Thread实现消息循环*/
  2. publicvoidmethodOne(Viewv){
  3. tag="methodOne";
  4. //创建一个LooperThread对象,实现了消息循环
  5. LooperThreadthread=newLooperThread();
  6. //必须启动这个线程
  7. thread.start();
  8. //创建一个消息对象并设置信息
  9. Messagemsg=newMessage();
  10. Bundlebundle=newBundle();
  11. bundle.putString("key","1.1Thread实现消息循环");
  12. msg.setData(bundle);
  13. //发送消息对象
  14. thread.mHandler.sendMessage(msg);
  15. }
  16. /**实现消息循环的线程(在Android索引文档android.os.Looper的概述里有介绍)*/
  17. privateclassLooperThreadextendsThread{
  18. publicHandlermHandler;
  19. publicvoidrun(){
  20. Looper.prepare();
  21. mHandler=newHandler(){
  22. publicvoidhandleMessage(Messagemsg){
  23. //processincomingmessageshere
  24. logThreadId();//打印当前线程ID
  25. Log.e(tag,msg.getData().getString("key"));
  26. }
  27. };
  28. Looper.loop();
  29. }
  30. }
如果不使用Looper.prepare()及loop()的话,就不能创建Handler将消息处理加入到Looper中。LogCat会报“Can't create handler inside thread that has not called Looper.prepare();”的错误信息。 2.2)Handler与Looper相关联 如果构造一个无参的Handler对象,它将自动与当前运行线程相关联。可以注意Handler相关例子中打印出的当前线程ID信息。 而与Looper相关联,需要创建一个带参数的Handler对象。注意:此时线程类应该是一个HandlerThread类,一个Looper类的Thread类。
        
  1. /**1.2Handler与Looper相关联*/
  2. publicvoidmethodTwo(Viewv){
  3. tag="methodTwo";
  4. //生成一个HandlerThread对象,使用Looper来处理消息队列
  5. HandlerThreadthread=newHandlerThread("MyThread");
  6. //必须启动这个线程
  7. thread.start();
  8. //将一个线程绑定到Handler对象上,则该Handler对象就可以处理线程的消息队列
  9. MyHandlermyhandler=newMyHandler(thread.getLooper());
  10. //从Handler中获取消息对象
  11. Messagemsg=myhandler.obtainMessage();
  12. //设置消息对象信息
  13. Bundlebundle=newBundle();
  14. bundle.putString("key","1.2Handler与Looper相关联");
  15. msg.setData(bundle);
  16. //将消息对象发送给目标对象Handler
  17. msg.sendToTarget();
  18. }
  19. /**重写Handler的消息处理方法*/
  20. privateclassMyHandlerextendsHandler{
  21. //带有参数的构造函数
  22. publicMyHandler(Looperlooper){
  23. super(looper);
  24. }
  25. @Override
  26. publicvoidhandleMessage(Messagemsg){
  27. //processincomingmessageshere
  28. logThreadId();//打印当前线程ID
  29. Log.e(tag,msg.getData().getString("key"));
  30. }
  31. }
所以,Wifi的HandlerThread,WifiHandler可以这样实例化(推荐博客有述):
        
  1. HandlerThreadwifiThread=newHandlerThread("WifiService");
  2. wifiThread.start();
  3. mWifiHandler=newWifiHandler(wifiThread.getLooper());
2.3)Hanlder与Thread实现异步 在新线程中执行耗时操作,结束后通过Handler来更新UI。
        
  1. /**1.3Hanlder与Thread实现异步*/
  2. publicvoidmethodThree(Viewv){
  3. tag="methodThree";
  4. /*初始化进度条*/
  5. progressBar.setProgress(0);
  6. progressBar.setVisibility(View.VISIBLE);
  7. //新线程执行某操作
  8. newProgressThread(0).start();
  9. }
  10. /**更新UI*/
  11. privateHandlermyHandler=newHandler(){
  12. @Override
  13. publicvoidhandleMessage(Messagemsg){
  14. progressBar.setProgress(msg.getData().getInt("key"));
  15. }
  16. };
  17. /**新线程任务*/
  18. privateclassProgressThreadextendsThread{
  19. privateintprogress;
  20. publicProgressThread(intprogress){
  21. this.progress=progress;
  22. }
  23. @Override
  24. publicvoidrun(){
  25. try{
  26. while(progress<=100){
  27. progress+=5;//进度+5
  28. //doSomething();//执行耗时操作
  29. Thread.sleep(100);
  30. //从Handler中获取消息对象
  31. Messagemsg=myHandler.obtainMessage();
  32. //设置消息对象信息
  33. Bundleb=newBundle();
  34. //向Handler发送消息,更新UI
  35. b.putInt("key",progress);
  36. msg.setData(b);
  37. myHandler.sendMessage(msg);
  38. }
  39. }catch(InterruptedExceptione){
  40. e.printStackTrace();
  41. }
  42. }
  43. }
3 )Android 异步线程——AsyncTask 当任务需要复杂操作并频繁更新UI时,上述的非主线程访问UI方法会使得代码结构复杂和难以理解。所以Android1.5提供了一个工具类AsyncTask,用以创建与用户界面交互的长时间任务。也在被定义在了package android.os。 AsyncTask定义了3种泛型类型:Params,ProgressandResult;4个执行步骤:onPreExecute,doInBackground,onProgressUpdateandonPostExecute。 3.1)泛型类型 1) Params,发送给后台任务处理的参数类型 2) Progress,后台任务过程中发布的进度单位 3) Result,后台任务结束后返回的结果类型
想了解泛型类型的话,可参见我的 Java泛型应用浅析一文^^。 3.2)执行步骤 1) onPreExecute(),该方法将在执行实际的后台操作前被UI thread调用。可以在该方法中做一些准备工作,如在界面上显示一个进度条。 2) doInBackground(Params...),将在onPreExecute方法执行后马上执行,该方法运行在后台线程中。这里将主要负责执行那些很耗时的后台计算工作。可以调用 publishProgress方法来更新实时的任务进度。该方法是抽象方法,子类必须实现。 3) onProgressUpdate(Progress...),在publishProgress方法被调用后,UI thread将调用这个方法从而在界面上展示任务的进展情况,例如通过一个进度条进行展示。 4) onPostExecute(Result),在doInBackground执行完成后,onPostExecute方法将被UI thread调用,后台的计算结果将通过该方法传递到UI thread。 onPostExecute的参数为doInBackground的返回值,类型由第三个泛型类型所指定。例子里仅是String,也可以是你自己的实体类、接口什么的。 3.3)使用准则 1) Task的实例必须在UI thread中创建 2) execute方法必须在UI thread中调用 3) 不要手动的调用onPreExecute(), onPostExecute(Result), doInBackground(Params...), onProgressUpdate(Progress...)这几个方法 4) 该task只能被执行一次,否则多次调用时将会出现异常 3.4)任务取消 一个任务在任何时候都能够通过调用cancel(boolean)而取消。调用这个方法,将使得随后调用的isCancelled()返回true。并且在执行完doInBackground(Object[])后,会去调用onCancelled(Object)而不再是onPostExecute(Object)方法。 为了确保任务能够尽快的被取消,可以在doInBackground(Object[])内定期校验isCancelled()的返回值(例如在循环判断中)。 3.5)样例程序
        
  1. /**2Android异步线程——AsyncTask*/
  2. publicvoidmethodFour(Viewv){
  3. tag="methodFour";
  4. newLoadHtmlTask(this).execute("http://www.baidu.com");//执行读取网页任务
  5. //newLoadHtmlTask(this).execute("http://www.google.com");//获取不到内容长度
  6. //newLoadHtmlTask(this).execute("http://www.sina.com");//获取大量网页数据
  7. }
  8. /**读取网页任务*/
  9. privateclassLoadHtmlTaskextendsAsyncTask<String,Integer,String>{
  10. privateContextmContext;
  11. privateProgressDialogdialog;//进度框
  12. publicLoadHtmlTask(Contextcontext){
  13. this.mContext=context;
  14. initDialog();//初始化进度对话框
  15. }
  16. /**初始化进度对话框*/
  17. privatevoidinitDialog(){
  18. dialog=newProgressDialog(mContext);
  19. dialog.setMax(100);//设置最大进度值
  20. dialog.setTitle("Loading...");//设置标题
  21. dialog.setCancelable(false);//设为返回键不可取消
  22. dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);//设为进度条样式
  23. //增加取消按钮及其事件
  24. dialog.setButton("取消",newDialogInterface.OnClickListener(){
  25. publicvoidonClick(DialogInterfacedialog,inti){
  26. dialog.dismiss();//取消显示
  27. cancel(true);//取消并中断任务
  28. }
  29. });
  30. }
  31. /**doInBackground之前,在主线程执行*/
  32. @Override
  33. protectedvoidonPreExecute(){
  34. logThreadId("onPreExecute()");//打印当前线程ID
  35. dialog.show();//显示进度对话框
  36. }
  37. /**onPreExecute()之后,在后台线程执行*/
  38. @Override
  39. protectedStringdoInBackground(String...params){
  40. logThreadId("doInBackground");//打印当前线程ID
  41. //未传入参数直接返回
  42. if(null==params||params.length<=0){
  43. return"请确认输入了网址参数!";
  44. }
  45. try{
  46. //创建HttpGet对象,params[0]为url
  47. HttpGethttpGet=newHttpGet(params[0]);
  48. //发送HttpGet请求,并返回HttpResponse对象
  49. HttpResponseresponse=newDefaultHttpClient()
  50. .execute(httpGet);
  51. //判断响应状态,200表示成功响应
  52. if(response.getStatusLine().getStatusCode()==200){
  53. HttpEntityentity=response.getEntity();//获取返回结果
  54. longlength=entity.getContentLength();//获取内容长度(google获取不到)
  55. booleangetLen=length>0?true:false;//判断是否获取了内容长度
  56. InputStreamis=entity.getContent();//获取响应内容
  57. if(is!=null){
  58. ByteArrayOutputStreambaos=newByteArrayOutputStream();
  59. byte[]buf=newbyte[128];
  60. intch=-1;
  61. longcount=0;
  62. while((ch=is.read(buf))!=-1&&!isCancelled()){//同时还未取消
  63. baos.write(buf,0,ch);
  64. count+=ch;
  65. if(getLen){//获取了内容长度时
  66. //调用publishProgress()更新进度
  67. publishProgress((int)((count/(float)length)*100));
  68. }
  69. }
  70. is.close();
  71. baos.close();
  72. returnnewString(baos.toByteArray());//返回结果
  73. }
  74. return"无返回内容!";
  75. }else{
  76. return"服务器未响应或失败!";
  77. }
  78. }catch(Exceptione){
  79. e.printStackTrace();
  80. }
  81. return"程序异常啦!";
  82. }
  83. /**调用了publishProgress(),在主线程执行*/
  84. @Override
  85. protectedvoidonProgressUpdate(Integer...values){
  86. //logThreadId("onProgressUpdate");//打印当前线程ID
  87. dialog.setProgress(values[0]);
  88. }
  89. /**未调用了cancel(),doInBackground()结束后,在主线程执行*/
  90. @Override
  91. protectedvoidonPostExecute(Stringresult){
  92. logThreadId("onPostExecute(result)");//打印当前线程ID
  93. dialog.dismiss();//取消显示
  94. showMsg(result);//显示结果
  95. }
  96. /**调用了cancel(),doInBackground()结束后,在主线程执行*/
  97. @Override
  98. protectedvoidonCancelled(){
  99. logThreadId("onCancelled");//打印当前线程ID
  100. showMsg("用户取消了该任务!");
  101. }
  102. /**提示框显示消息*/
  103. privatevoidshowMsg(Stringmessage){
  104. //message内容过多时,提示框显示会延迟,例如sina
  105. newAlertDialog.Builder(mContext)
  106. .setTitle("消息")
  107. .setMessage(message)
  108. .setNegativeButton("关闭",
  109. newDialogInterface.OnClickListener(){
  110. @Override
  111. publicvoidonClick(DialogInterfacedialog,
  112. intwhich){
  113. dialog.dismiss();
  114. }
  115. }).show();
  116. }
  117. }
三、扩展篇 1)Service中的Toast Service中不能直接使用Toast提示信息,推荐如下方式:
        
  1. privateHandlerhandler;//Handler
  2. Override
  3. publicvoidonCreate(){
  4. Log.i("onCreate","==onCreate==");
  5. super.onCreate();
  6. handler=newHandler(Looper.getMainLooper());//使用应用的主消息循环
  7. }
  8. /**
  9. *Toast提示(service中toast不能直接显示)
  10. */
  11. privatevoidshowToast(finalintresId,finalObject...formatArgs){
  12. handler.post(newRunnable(){
  13. publicvoidrun(){
  14. Toast.makeText(getApplicationContext(),
  15. getString(resId,formatArgs),Toast.LENGTH_SHORT)
  16. .show();
  17. }
  18. });
  19. //以下方式只能显示一次
  20. //Looper.prepare();
  21. //Toast.makeText(this,resId,Toast.LENGTH_SHORT).show();
  22. //Looper.loop();
  23. }

2)Java线程池 一些简单常用的线程池,只需使用Executors类里面提供了一些静态工厂,如下: 1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 3)newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。 4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 5)newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。 或者使用ThreadPoolExecutor,更加定制化地构造线程池。它们都被定义在package java.util.concurrent。 线程池技术是为了减少频繁创建和销毁线程的系统开销,适用情况有:1)单个任务时间很短、处理请求巨大;2)有突发性大量任务请求;3)需要迅速响应的性能要求等。
        
  1. publicclassTestPoolimplementsRunnable{
  2. privatestaticfinalStringTAG="TestPool";//标记
  3. privateExecutorServiceservice;//线程池
  4. publicTestPool(){
  5. //service=Executors.newSingleThreadExecutor();//创建一个单任务线程池
  6. service=Executors.newFixedThreadPool(3);//创建最多同时运行3个任务线程池
  7. }
  8. /**增加一个线程任务*/
  9. publicvoidaddTask(){
  10. service.execute(this);
  11. }
  12. /**线程任务*/
  13. @Override
  14. publicvoidrun(){
  15. log(1);//显示日志
  16. try{
  17. Thread.sleep(5*1000);
  18. }catch(InterruptedExceptione){
  19. e.printStackTrace();
  20. }
  21. log(2);//显示日志
  22. }
  23. /**显示日志*/
  24. privatevoidlog(intwhich){
  25. Stringresult=1==which?",任务开始!":",任务结束!";
  26. Log.e(TAG,"线程:"+String.valueOf(Thread.currentThread().getId())
  27. +result);
  28. }
  29. }
3)Java线程同步 多线程并发访问同一数据时,就会有同步的需求。Java内在的同步机制包含:同步块(或方法)和 volatile 变量。同步块(或方法)通过synchronized关键字声明,而volatile可被看做是轻量级的synchronized。 Java为每个object分配了一个monitor,相关方法如下: 1)obj.wait()方法将使本线程挂起,并释放obj对象的monitor。只有其他线程调用obj对象的notify()或notifyAll()时,才可以被唤醒。 2)obj.notifyAll()方法唤醒所有该obj对象相关的沉睡线程,然后被唤醒的众多线程开始竞争obj对象的monitor占有权,最终得到的那个线程会继续执行下去,但其他线程还将继续等待。 3)obj.notify()方法是随机唤醒一个沉睡线程。 4)wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用。 没多线程不需要同步,有多线程不一定需要同步。
        
  1. publicclassTestSync{
  2. privateProductproduct;
  3. privateConsumerConsumerA,ConsumerB;
  4. publicTestSync(){
  5. product=newProduct();
  6. ConsumerA=newConsumer("小怪兽A",product);
  7. ConsumerB=newConsumer("小怪兽B",product);
  8. }
  9. publicvoidproduce(){
  10. synchronized(product){
  11. Log.e("TestSync","\\(^o^)/,投掷一个果冻!");
  12. product.plus();//增加一个产品
  13. /*优先使用notifyAll(),更容易让jvm找到最适合被唤醒的线程*/
  14. //product.notify();//唤醒一个线程
  15. product.notifyAll();//唤醒所有线程
  16. }
  17. }
  18. publicvoidstart(){
  19. ConsumerA.start();
  20. ConsumerB.start();
  21. }
  22. publicvoidstop(){
  23. ConsumerA.stopEating();
  24. ConsumerB.stopEating();
  25. synchronized(product){
  26. product.notifyAll();//唤醒所有线程
  27. }
  28. }
  29. }
  30. /**产品*/
  31. classProduct{
  32. privateintcount=0;//产品数量
  33. publicProduct(){
  34. }
  35. /**是否无产品*/
  36. publicbooleanisNull(){
  37. returncount<=0?true:false;
  38. }
  39. /**增加产品*/
  40. publicvoidplus(){
  41. count++;
  42. }
  43. /**减少产品*/
  44. publicvoidminus(){
  45. count--;
  46. }
  47. }
  48. /**消费者*/
  49. classConsumerextendsThread{
  50. privateStringname;//消费者
  51. privateProductproduct;//产品
  52. privateintcount=0;//数量
  53. privatebooleanwaitEating=true;//标记
  54. publicConsumer(Stringname,Productproduct){
  55. this.name=name;
  56. this.product=product;
  57. }
  58. @Override
  59. publicvoidrun(){
  60. while(waitEating){
  61. synchronized(product){
  62. while(product.isNull()&&waitEating){
  63. try{
  64. Log.e(name,"(¯��¯),等待果冻中...");
  65. product.wait();
  66. }catch(InterruptedExceptione){
  67. e.printStackTrace();
  68. }
  69. }
  70. if(waitEating){
  71. Log.e(name,"~\\(�R��Q)/~,抢到了果冻!");
  72. product.minus();
  73. count++;
  74. }
  75. }
  76. }
  77. Log.e(name,"(~o~)~zZ,吃不下了。计:"+count);
  78. }
  79. publicvoidstopEating(){
  80. waitEating=false;
  81. }
  82. }

更多相关文章

  1. 【Android中Broadcast Receiver组件具体解释 】
  2. 【转】Invalidate和postInvalidate的区别
  3. 框架层理解Activity生命周期(APP启动过程)
  4. 关于activity和task的设计思路和方法
  5. android游戏绘制屏幕
  6. Android绘图基础:Canvas、Paint、Path的简单使用
  7. 【Android】EventBus 3.0 源码分析
  8. [Android]Uri、UriMatcher、ContentUris详解
  9. flutter 适配Android(安卓)ios全面屏

随机推荐

  1. 研究显示 iOS 应用比 Android 应用更易崩
  2. android 时间控件概述
  3. Android与iOS的对决
  4. Android应用程序窗口View的创建过程
  5. 系统编译三方apk,out目录有这个apk,但刷机
  6. Android培训班(12)
  7. Android 4.4 以太网网络共享功能研究和实
  8. Android传感器编程实例开发——三轴数据
  9. 移动开发 - Android - 实现两个页面(Acti
  10. 【攻克Android (2)】Android各版本、app