life02

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  197 随笔 :: 3 文章 :: 37 评论 :: 0 Trackbacks
Android面试题
http://topic.csdn.net/u/20101219/21/b6f83d6f-35b5-49ea-b281-d47ca662ea17.html?57324
1. 请描述下Activity的生命周期。
2. 如果后台的Activity由于某原因被系统回收了,如何在被系统回收之前保存当前状态?
3. 如何将一个Activity设置成窗口的样式。(Edited by Sodino)
4. 如何退出Activity?如何安全退出已调用多个Activity的Application?
5. 请介绍下Android中常用的五种布局。
6. 请介绍下Android的数据存储方式。(Edited by Sodino)
7. 请介绍下ContentProvider是如何实现数据共享的。(Edited by Sodino)
8. 如何启用Service,如何停用Service。(Edited by Sodino)
9. 注册广播有几种方式,这些方式有何优缺点?请谈谈Android引入广播机制的用意。
10. 请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系。
11. AIDL的全称是什么?如何工作?能处理哪些类型的数据?
12. 请解释下Android程序运行时权限与文件系统权限的区别。(Edited by Sodino)
13. 系统上安装了多种浏览器,能否指定某浏览器访问指定页面?请说明原由。
14. 有一个一维整型数组int[]data保存的是一张宽为width,高为height的图片像素值信息。请写一个算法,将该图片所有的白色不透明(0xffffffff)像素点的透明度调整为50%。
15. 你如何评价Android系统?优缺点。


1.activity的生命周期。 

2.横竖屏切换时候activity的生命周期 

总结:

1、不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次

2、设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次

3、设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法


3.android中的动画有哪几类,它们的特点和区别是什么 


4.handler机制的原理 


5.说说activity,intent,service是什么关系 

6.android中线程与线程,进程与进程之间如何通信 

7.widget相对位置的完成在antivity的哪个生命周期阶段实现 

8.说说mvc模式的原理,它在android中的运用 

9.说说在android中有哪几种数据存储方式 

10.android中有哪几种解析xml的类,官方推荐哪种?以及它们的原理和区别

3. 如何将一个Activity设置成窗口的样式。
  设置Activity窗口化:
  android:theme="@android:style/Theme.Dialog"
  设置Activity透明化:  
  android:theme="@android:style/Theme.Translucent"
6. 请介绍下Android的数据存储方式
  ㈠.文件存储方式:
在Android中通常使用File存储方式是用 Context.openFileOutput(String fileName, int mode)和Context.openFileInput(String fileName)。
  Context.openFileOutput(String fileName, int mode)生成的文件自动存储在/data/data/PackageName/file目录下,其全路径是/data/data/Package Name/files/fileName 。注意下,这里的参数fileName不可以包含路径分割符(如"/")。
  通常来说,这种方式生成的文件只能在这个apk内访问。但这个结论是指使用Context.openFileInput(String fileName)的方式。使用这种方式,每个apk只可以访问自己的/data/data/Package Name/files目录下的文件,原因很简单,参数fileName中不可以包含路径分割符,Android会自动在/data/data /Package Name/files目录下寻找文件名为fileName的文件。
具体实例如下:
String fn = “moandroid.log”;
FileInputStream fis = openFileInput(fn);
FileOutputStream fos = openFileOutput(fn.Context.MODE_PRIVATE);

㈡.使用SharedPreferences存储数据
首先说明SharedPreferences存储方式,它是 Android提供的用来存储一些简单配置信息的一种机制,例如:登录用户的用户名与密码。其采用了Map数据结构来存储数据,以键值的方式存储,可以简单的读取与写入,具体实例如下:
  void ReadSharedPreferences(){
  String strName,strPassword;
  SharedPreferences user = getSharedPreferences(“user_info”,0);
  strName = user.getString(“NAME”,””);
  strPassword = user getString(“PASSWORD”,””);
  }
  void WriteSharedPreferences(String strName,String strPassword){
  SharedPreferences user = getSharedPreferences(“user_info”,0);
  uer.edit()//获得编辑器;
  user.putString(“NAME”, strName);
  user.putString(“PASSWORD” ,strPassword);
  user.commit();
  }
  数据读取与写入的方法都非常简单,只是在写入的时候有些区别:先调用edit()使其处于编辑状态,然后才能修改数据,最后使用commit()提交修改的数据。实际上SharedPreferences是采用了XML格式将数据存储到设备中,在DDMS中的File Explorer中的/data/data/<package name>/shares_prefs下。以上面的数据存储结果为例,打开后可以看到一个user_info.xml的文件,打开后可以看到:
  <?xml version=”1.0″ encoding=”UTF-8″?>
  <map>
  <string name=”NAME”>moandroid</string>
  <string name=” PASSWORD”>SharedPreferences</string>
  </map>
使用SharedPreferences是有些限制的:只能在同一个包内使用,不能在不同的包之间使用。

㈢:网络存储数据
网络存储方式,需要和Android网络数据包打交道。

㈣.ContentProvider数据存储
1、ContentProvider简介
当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。与之相关联的是uri,下面是uri的简介:
2、Uri类简介
Uri代表了要操作的数据,Uri主要包含了两部分信息:需要操作的ContentProvider和对ContentProvider中的什么数据进行操作,一个Uri由以下几部分组成:
  1).scheme:ContentProvider(内容提供者)的scheme已经由Android所规定为:content://。
  2).主机名(或Authority):用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。
  3).路径(path):可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下:
  要操作contact表中id为10的记录,可以构建这样的路径:/contact/10
  要操作contact表中id为10的记录的name字段, contact/10/name
  要操作contact表中的所有记录,可以构建这样的路径:/contact?
  要操作的数据不一定来自数据库,也可以是文件等他存储方式,如下:
  要操作xml文件中contact节点下的name节点,可以构建这样的路径:/contact/name
如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下:
Uri uri = Uri.parse("content://com.changcheng.provider.contactprovider/contact")
  3、UriMatcher、ContentUrist和ContentResolver简介
因为Uri代表了要操作的数据,所以我们很经常需要解析Uri,并从 Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher 和ContentUris 。掌握它们的使用,会便于我们的开发工作。
? UriMatcher:用于匹配Uri,它的用法如下:
  1).首先把你需要匹配Uri路径全部给注册上,如下:
//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码(-1)。
UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配content://com.changcheng.sqlite.provider.contactprovider /contact路径,返回匹配码为1
uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact”, 1);//添加需要匹配uri,如果匹配就会返回匹配码
//如果match()方法匹配 content://com.changcheng.sqlite.provider.contactprovider/contact/230路径,返回匹配码为2
uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact/#”, 2);//#号为通配符

  2).注册完需要匹配的Uri后,就可以使用uriMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配就返回匹配码,匹配码是调用 addURI()方法传入的第三个参数,假设匹配 content://com.changcheng.sqlite.provider.contactprovider/contact路径,返回的匹配码为1。

ContentUris:用于获取Uri路径后面的ID部分,它有两个比较实用的方法:
 withAppendedId(uri, id)用于为路径加上ID部分
 parseId(uri)方法用于从路径中获取ID部分
 ContentResolver:当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用 ContentResolver 类来完成,要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。 ContentResolver使用insert、delete、update、query方法,来操作数据。
㈤.数据库存储方式?
数据库存储方式,最长用的是SQLite
8. 如何启用Service,如何停用Service
  ● 启动方式和停止方式:
㈠.Context.startService() / Context.stopService();
㈡. Context.bindService() / Context.unbindService();
9. 注册广播有几种方式,这些方式有何优缺点?请谈谈Android引入广播机制的用意。
  在android下,要想接受广播信息,那么这个广播接收器就得我们自己来实现了,我们可以继承BroadcastReceiver,就可以有一个广播接受器了。有个接受器还不够,我们还得重写BroadcastReceiver里面的onReceiver方法,当来广播的时候我们要干什么,这就要我们自己来实现,不过我们可以搞一个信息防火墙。具体的代码:
public class SmsBroadCastReceiver extends BroadcastReceiver  
{  
  public void onReceive(Context context, Intent intent)  
  {  
  Bundle bundle = intent.getExtras();  
  Object[] object = (Object[])bundle.get("pdus");  
  SmsMessage sms[]=new SmsMessage[object.length];  
  for(int i=0;i<object.length;i++)  
  {  
  sms[0] = SmsMessage.createFromPdu((byte[])object[i]);  
  Toast.makeText(context, "来自 "+sms[i].getDisplayOriginatingAddress()+" 的消息是:"+sms[i].getDisplayMessageBody(), Toast.LENGTH_SHORT).show();  
  }  
  //终止广播,在这里我们可以稍微处理,根据用户输入的号码可以实现短信防火墙。  
  abortBroadcast();  
  }  
   
}  
当实现了广播接收器,还要设置广播接收器接收广播信息的类型,这里是信息:android.provider.Telephony.SMS_RECEIVED
  我们就可以把广播接收器注册到系统里面,可以让系统知道我们有个广播接收器。这里有两种,
? 一种是代码动态注册:
//生成广播处理  
smsBroadCastReceiver = new SmsBroadCastReceiver();  
//实例化过滤器并设置要过滤的广播  
IntentFilter intentFilter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED"); 

//注册广播  
BroadCastReceiverActivity.this.registerReceiver(smsBroadCastReceiver, intentFilter);  
? 一种是在AndroidManifest.xml中配置广播
<?xml version="1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="spl.broadCastReceiver"  
  android:versionCode="1"  
  android:versionName="1.0">  
  <application android:icon="@drawable/icon" android:label="@string/app_name">  
  <activity android:name=".BroadCastReceiverActivity"  
  android:label="@string/app_name">  
  <intent-filter>  
  <action android:name="android.intent.action.MAIN" />  
  <category android:name="android.intent.category.LAUNCHER" />  
  </intent-filter>  
  </activity>  
   
  <!--广播注册-->  
  <receiver android:name=".SmsBroadCastReceiver">  
  <intent-filter android:priority="20">  
  <action android:name="android.provider.Telephony.SMS_RECEIVED"/>  
  </intent-filter>  
  </receiver>  
   
  </application>  
   
  <uses-sdk android:minSdkVersion="7" />  
   
  <!-- 权限申请 -->  
  <uses-permission android:name="android.permission.RECEIVE_SMS"></uses-permission>  
   
</manifest>  
  两种注册类型的区别是:
  1)第一种不是常驻型广播,也就是说广播跟随程序的生命周期。
  2)第二种是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行
请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系。
  2.2 Message Queue
  在单线程模型下,为了解决类似的问题,Android设计了一个Message Queue(消息队列), 线程间可以通过该Message Queue并结合Handler和Looper组件进行信息交换。下面将对它们进行分别介绍:
1. Message
  Message消息,理解为线程间交流的信息,处理数据后台线程需要更新UI,则发送Message内含一些数据给UI线程。
2. Handler
  Handler处理者,是Message的主要处理者,负责Message的发送,Message内容的执行处理。后台线程就是通过传进来的 Handler对象引用来sendMessage(Message)。而使用Handler,需要implement 该类的 handleMessage(Message)
方法,它是处理这些Message的操作内容,例如Update UI。通常需要子类化Handler来实现handleMessage方法。
3. Message Queue
  Message Queue消息队列,用来存放通过Handler发布的消息,按照先进先出执行。
  每个message queue都会有一个对应的Handler。Handler会向message queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在message queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个message对象,会被 Handler的handleMessage()函数处理;而通过post方法发送的是一个runnable对象,则会自己执行。
4. Looper
  Looper是每条线程里的Message Queue的管家。Android没有Global的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()得到的主线程的Looper不为NULL,但调用Looper.myLooper() 得到当前线程的Looper就有可能为NULL。
  对于子线程使用Looper,API Doc提供了正确的使用方法:

  这个Message机制的大概流程:
  1. 在Looper.loop()方法运行开始后,循环地按照接收顺序取出Message Queue里面的非NULL的Message。
  2. 一开始Message Queue里面的Message都是NULL的。当Handler.sendMessage(Message)到Message Queue,该函数里面设置了那个Message对象的target属性是当前的Handler对象。随后Looper取出了那个Message,则调用该Message的target指向的Hander的dispatchMessage函数对Message进行处理。
  在dispatchMessage方法里,如何处理Message则由用户指定,三个判断,优先级从高到低:
  1) Message里面的Callback,一个实现了Runnable接口的对象,其中run函数做处理工作;
  2) Handler里面的mCallback指向的一个实现了Callback接口的对象,由其handleMessage进行处理;
  3) 处理消息Handler对象对应的类继承并实现了其中handleMessage函数,通过这个实现的handleMessage函数处理消息。
  由此可见,我们实现的handleMessage方法是优先级最低的!
  3. Handler处理完该Message (update UI) 后,Looper则设置该Message为NULL,以便回收!
  在网上有很多文章讲述主线程和其他子线程如何交互,传送信息,最终谁来执行处理信息之类的,个人理解是最简单的方法——判断Handler对象里面的 Looper对象是属于哪条线程的,则由该线程来执行!
  1. 当Handler对象的构造函数的参数为空,则为当前所在线程的Looper;
  2. Looper.getMainLooper()得到的是主线程的Looper对象,Looper.myLooper()得到的是当前线程的Looper对象。

11. AIDL的全称是什么?如何工作?能处理哪些类型的数据?
  AIDL(AndRoid接口描述语言)是一种接口描述语言; 编译器可以通过aidl文件生成一段代码,通过预先定义的接口达到两个进程内部通信进程的目的. 如果需要在一个Activity中, 访问另一个Service中的某个对象, 需要先将对象转化成AIDL可识别的参数(可能是多个参数), 然后使用AIDL来传递这些参数, 在消息的接收端, 使用这些参数组装成自己需要的对象.
AIDL的IPC的机制和COM或CORBA类似, 是基于接口的,但它是轻量级的。它使用代理类在客户端和实现层间传递值. 如果要使用AIDL, 需要完成2件事情: 1. 引入AIDL的相关类.; 2. 调用aidl产生的class.
AIDL的创建方法:
AIDL语法很简单,可以用来声明一个带一个或多个方法的接口,也可以传递参数和返回值。由于远程调用的需要, 这些参数和返回值并不是任何类型.
下面是些AIDL支持的数据类型:
1. 不需要import声明的简单Java编程语言类型(int,boolean等)
2. String, CharSequence不需要特殊声明 
3. List, Map和Parcelables类型, 这些类型内所包含的数据成员也只能是简单数据类型, String等其他比支持的类型.
12. 请解释下Android程序运行时权限与文件系统权限的区别
  要区分apk运行时的拥有的权限与在文件系统上被访问(读写执行)的权限两个概念。
  apk程序是运行在虚拟机上的,对应的是Android独特的权限机制,只有体现到文件系统上时才使用linux的权限设置
(一)linux文件系统上的权限
-rwxr-x--x system system 4156 2010-04-30 16:13 test.apk
  代表的是相应的用户/用户组及其他人对此文件的访问权限,与此文件运行起来具有的权限完全不相关。比如上面的例子只能说明system用户拥有对此文件的读写执行权限;system组的用户对此文件拥有读、执行权限;其他人对此文件只具有执行权限。
而test.apk运行起来后可以干哪些事情,跟这个就不相关了。
千万不要看apk文件系统上属于system/system用户及用户组,或者root/root用户及用户组,就认为apk具有system或root权限
  (二)Android的权限规则
  (1)Android中的apk必须签名
  这种签名不是基于权威证书的,不会决定某个应用允不允许安装,而是一种自签名证书。重要的是,android系统有的权限是基于签名的。比如:system等级的权限有专门对应的签名,签名不对,权限也就获取不到。
默认生成的APK文件是debug签名的。
(2)基于UserID的进程级别的安全机制
大家都知道,进程有独立的地址空间,进程与进程间默认是不能互相访问的,是一种很可靠的保护机制。
Android通过为每一个安装在设备上的包(apk)分配唯一的linux userID来实现,名称为"app_"加一个数字,比如app_43
不同的UserID,运行在不同的进程,所以apk之间默认便不能相互访问。
Android提供了如下的一种机制,可以使两个apk打破前面讲的这种壁垒。
在AndroidManifest.xml中利用sharedUserId属性给不同的package分配相同的userID,通过这样做,两个package可以被当做同一个程序,
系统会分配给两个程序相同的UserID。当然,基于安全考虑,两个package需要有相同的签名,否则没有验证也就没有意义了。
(这里补充一点:并不是说分配了同样的UserID,两程序就运行在同一进程, 下面为PS指令摘取的,
显然,system、app_2分别对应的两个进程的PID都不同,不知Android到底是怎样实现它的机制的)
User PID PPID
system 953 883 187340 55052 ffffffff afe0cbcc S system_server
app_2 1072 883 100264 19564 ffffffff afe0dcc4 S com.android.inputmethod.
system 1083 883 111808 23192 ffffffff afe0dcc4 S android.process.omsservi
app_2 1088 883 156464 45720 ffffffff afe0dcc4 S android.process.acore
(3)默认apk生成的数据对外是不可见的
实现方法是:Android会为程序存储的数据分配该程序的UserID。
借助于Linux严格的文件系统访问权限,便实现了apk之间不能相互访问似有数据的机制。
例:我的应用创建的一个文件,默认权限如下,可以看到只有UserID为app_21的程序才能读写该文件。
-rw------- app_21 app_21 87650 2000-01-01 09:48 test.txt
如何对外开放?
<1> 使用MODE_WORLD_READABLE and/or MODE_WORLD_WRITEABLE 标记。
When creating a new file with getSharedPreferences(String, int), openFileOutput(String, int), or openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory), you can use the MODE_WORLD_READABLE and/or MODE_WORLD_WRITEABLE flags to allow any other package to read/write the file. When setting these flags, the file is still owned by your application, but its global read and/or write permissions have been set appropriately so any other application can see it.

(4)AndroidManifest.xml中的显式权限声明
Android默认应用是没有任何权限去操作其他应用或系统相关特性的,应用在进行某些操作时都需要显式地去申请相应的权限。
一般以下动作时都需要申请相应的权限:
A particular permission may be enforced at a number of places during your program's operation: 
• At the time of a call into the system, to prevent an application from executing certain functions.
• When starting an activity, to prevent applications from launching activities of other applications.
• Both sending and receiving broadcasts, to control who can receive your broadcast or who can send a broadcast to you.
• When accessing and operating on a content provider.
• Binding or starting a service.

在应用安装的时候,package installer会检测该应用请求的权限,根据该应用的签名或者提示用户来分配相应的权限。
在程序运行期间是不检测权限的。如果安装时权限获取失败,那执行就会出错,不会提示用户权限不够。
大多数情况下,权限不足导致的失败会引发一个 SecurityException, 会在系统log(system log)中有相关记录。
(5)权限继承/UserID继承
当我们遇到apk权限不足时,我们有时会考虑写一个linux程序,然后由apk调用它去完成某个它没有权限完成的事情,很遗憾,这种方法是行不通的。
前面讲过,android权限是经营在进程层面的,也就是说一个apk应用启动的子进程的权限不可能超越其父进程的权限(即apk的权限),
即使单独运行某个应用有权限做某事,但如果它是由一个apk调用的,那权限就会被限制。
实际上,android是通过给子进程分配父进程的UserID实现这一机制的。
(三)常见权限不足问题分析

首先要知道,普通apk程序是运行在非root、非system层级的,也就是说看要访问的文件的权限时,看的是最后三位。
另外,通过system/app安装的apk的权限一般比直接安装或adb install安装的apk的权限要高一些。

言归正传,运行一个android应用程序过程中遇到权限不足,一般分为两种情况:
(1)Log中可明显看到权限不足的提示。
此种情况一般是AndroidManifest.xml中缺少相应的权限设置,好好查找一番权限列表,应该就可解决,是最易处理的情况。
有时权限都加上了,但还是报权限不足,是什么情况呢?
Android系统有一些API及权限是需要apk具有一定的等级才能运行的。
比如 SystemClock.setCurrentTimeMillis()修改系统时间,WRITE_SECURE_SETTINGS权限好像都是需要有system级的权限才行。
也就是说UserID是system.
(2)Log里没有报权限不足,而是一些其他Exception的提示,这也有可能是权限不足造成的。
比如:我们常会想读/写一个配置文件或其他一些不是自己创建的文件,常会报java.io.FileNotFoundException错误。
系统认为比较重要的文件一般权限设置的也会比较严格,特别是一些很重要的(配置)文件或目录。

-r--r----- bluetooth bluetooth 935 2010-07-09 20:21 dbus.conf
drwxrwx--x system system 2010-07-07 02:05 data 

dbus.conf好像是蓝牙的配置文件,从权限上来看,根本就不可能改动,非bluetooth用户连读的权利都没有。
/data目录下存的是所有程序的私有数据,默认情况下android是不允许普通apk访问/data目录下内容的,通过data目录的权限设置可知,其他用户没有读的权限。
所以adb普通权限下在data目录下敲ls命令,会得到opendir failed, Permission denied的错误,通过代码file.listfiles()也无法获得data目录下的内容。
13. 系统上安装了多种浏览器,能否指定某浏览器访问指定页面?请说明原由。
  如果在你的android系统上安装了多种浏览器,能否指定某浏览器访问指定页面?答案当然是:肯定的。
问题的关键在于我们设置了class name,也就是我们想要跳转的pakcage的activity。如果你想要跳转到其它的浏览器,只需要修改一下这个函数就OK了。
  好,我们现在来让刚刚的思路来指导我们的实践。假如我们现在要直接启动UC浏览器,那么我们该怎么做呢?让我们step by step吧。

1.下载UC apk:http://i-uc.net/read.php?2

2. 用7zip解压apk文件,得到classes.dex文件

3.下载反编译dex文件工具:http://nchc.dl.sourceforge.net/project/dedexer/dedexer/1.5/ddx1.5.jar(Dedexer 项目主页: http://dedexer.sourceforge.net/)
4.执行命令:java -jar ddx1.5.jar -o -D -d c:\ c:\classes.dex
5. 得到package name是:com.uc.browser,启动的activity是:com.uc.browser.ActivityUpdate(补充:当我在这里选择采用ActivityBrowser的时候发觉权限不够,报permiss denied 异常,而且也不是我们要的那个activity,幸运的是在第二次尝试用ActivityUpdate,刚好能满足要求)
6.修改上面的代码为intent.setClassName("com.uc.browser","com.uc.browser.ActivityUpdate");
posted on 2011-12-06 16:38 life02 阅读(853) 评论(0)  编辑 收藏 引用 所属分类: android面试题

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理