Android常见面试问题汇总
内存存储方式
一、可编程内存在基本上分为这样的几大部分:静态存储区、堆区和栈区。
静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。
栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。
二、三者之间的区别
我们通过代码段来看看对这样的三部分内存需要怎样的操作和不同,以及应该注意怎样的地方。
例一:静态存储区与栈区
char* p = “Hello World1”;
char a[] = “Hello World2”;
p[2] = ‘A’;
a[2] = ‘A’;
char* p1 = “Hello World1;”
这个程序是有错误的,错误发生在p[2] = ‘A’这行代码处,为什么呢,是变量p和变量数组a都存在于栈区的(任何临时变量都是处于栈区的,包括在main()函数中定义的变量)。但是,数据“Hello World1”和数据“Hello World2”是存储于不同的区域的。
因为数据“Hello World2”存在于数组中,所以,此数据存储于栈区,对它修改是没有任何问题的。因为指针变量p仅仅能够存储某个存储空间的地址,数据“Hello World1”为字符串常量,所以存储在静态存储区。虽然通过p[2]可以访问到静态存储区中的第三个数据单元,即字符‘l’所在的存储的单元。但是因为数据“Hello World1”为字符串常量,不可以改变,所以在程序运行时,会报告内存错误。并且,如果此时对p和p1输出的时候会发现p和p1里面保存的地址是完全相同的。换句话说,在数据区只保留一份相同的数据(见图1-1)。
例二:栈区与堆区
char* f1()
{
char* p = NULL;
char a;
p = &a;
return p;
}
char* f2()
{
char* p = NULL:
p =(char*) new char[4];
return p;
}
这两个函数都是将某个存储空间的地址返回,二者有何区别呢?f1()函数虽然返回的是一个存储空间,但是此空间为临时空间。也就是说,此空间只有短暂的生命周期,它的生命周期在函数f1()调用结束时,也就失去了它的生命价值,即:此空间被释放掉。所以,当调用f1()函数时,如果程序中有下面的语句:
char* p ;
p = f1();
*p = ‘a’;
此时,编译并不会报告错误,但是在程序运行时,会发生异常错误。因为,你对不应该操作的内存(即,已经释放掉的存储空间)进行了操作。但是,相比之下,f2()函数不会有任何问题。因为,new这个命令是在堆中申请存储空间,一旦申请成功,除非你将其delete或者程序终结,这块内存将一直存在。也可以这样理解,堆内存是共享单元,能够被多个函数共同访问。如果你需要有多个数据返回却苦无办法,堆内存将是一个很好的选择。但是一定要避免下面的事情发生:
void f()
{
…
char * p;
p = (char*)new char[100];
…
}
这个程序做了一件很无意义并且会带来很大危害的事情。因为,虽然申请了堆内存,p保存了堆内存的首地址。但是,此变量是临时变量,当函数调用结束时p变量消失。也就是说,再也没有变量存储这块堆内存的首地址,我们将永远无法再使用那块堆内存了。但是,这块堆内存却一直标识被你所使用(因为没有到程序结束,你也没有将其delete,所以这块堆内存一直被标识拥有者是当前您的程序),进而其他进程或程序无法使用。我们将这种不道德的“流氓行为”(我们不用,却也不让别人使用)称为内存泄漏。这是我们C++程序员的大忌!!请大家一定要避免这件事情的发生。
总之,对于堆区、栈区和静态存储区它们之间最大的不同在于,栈的生命周期很短暂。但是堆区和静态存储区的生命周期相当于与程序的生命同时存在(如果您不在程序运行中间将堆内存 3ff7 delete的话),我们将这种变量或数据成为全局变量或数据。但是,对于堆区的内存空间使用更加灵活,因为它允许你在不需要它的时候,随时将它释放掉,而静态存储区将一直存在于程序的整个生命周期中。
希望大家记住下面的规则:
【规则1】用malloc 或new 申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL 的内存。
【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
【规则4】动态内存的申请与释放必须配对,防止内存泄漏。
【规则5】用free 或delete 释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
堆与栈的讨论:
管理方式:
堆中资源由程序员控制(容易产生memory leak)。
栈资源由编译器自动管理,无需手工控制。
系统响应:
对于堆,应知道系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中)。
对于栈,只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。
空间大小:
堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit系统理论上是4G),所以堆的空间比较灵活,比较大。
栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译时确定,VC中可设置)。
碎片问题:
对于堆,频繁的new/delete会造成大量碎片,使程序效率降低。
对于栈,它是一个先进后出的队列,进出一一对应,不会产生碎片。
生长方向:
堆向上,向高地址方向增长。
栈向下,向低地址方向增长。
分配方式:
堆都是动态分配(没有静态分配的堆)。
栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。
分配效率:
堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多。
栈是极其系统提供的数据结构,计算机在底层对栈提供支持,分配专门寄存器存放栈地址,栈操作有专门指令
局部变量,静态变量,全局变量
把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。
把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。
面向对象思想
封装,继承,多态
1. 封装:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private, protected,public)
2. 继承:广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。
3. 多态:是将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
public interface Parent//父类接口
{
publicvoidsimpleCall();
}
public class Child_A implements Parent
{
publicvoidsimpleCall()
{
//具体的实现细节;
}
}
public class Child_B implements Parent
{
publicvoidsimpleCall()
{
//具体的实现细节;
}
}
重载(overload)和重写/覆盖(overried)
重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
重写:是指子类重新定义父类虚函数的方法。
从实现原理上来说:
重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关 !
重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。
类成员函数的重载、覆盖和隐藏区别
a.成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
b.覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
引用与指针异同点
引用:
int a = 3;
int& b = a;//储存的是值
指针:
int a = 3;
int* b = &a;//储存的是地址
Ø 相同点:
1.一个const引用可以引用一个非const变量
int a = 3;
const int& b = a;
一个const指针可以指向一个非const变量
int a = 3;
const int* b = &a;
2.一个const引用可以引用一个const变量
const int a = 3;
const int& b = a;
一个const指针可以指向一个const变量
const int a = 3;
const int* b = &a;
3.一个非const引用不可以引用一个const变量
const int a = 3;
int& b = a;//是错误的,可以通过b来改变的a的值,但是a在这里是常量,不可以改变
一个非const指针不可以引用一个cosnt变量
const int a = 3;
int* b = &a;//是错误的,可以通过b来改变的a的值,但是a在这里是常量,不可以改变
4.指向的都是变量的内存地址。
Ø 不同点
1.引用是一个变量的另外一个名字
定义方式:
int a = 3;
int& b = a;//储存的是值
指针是一个变量的拷贝,复制体
int a = 3;
int* b = &a;//储存的是地址
2.定义:
引用只能对已经存在的变量或对象实现引用
而指针则不需要,可以定义为空;
3.传参:
如果传递的是一个引用,意味着这个变量或对象已经存在了;
如果传递的是一个指针,则不能判断这个指针是不是有效的,是不是空的 ... ...
所以,引用较为安全。
虽然传引用较为安全,但是也是利用指针来实现的,所以指针的效率要比引用高一些。
4.引用直接去访问变量,不用分配自己的内存空间,而指针是间接访问,需要有自己的内存空间
内存分配方式以及它们的区别
1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也 8000 最多。
如何判断一个单链表是有环的?
(注意不能用标志位,最多只能用两个额外指针)
/****************************************************************
return false : 无环;true: 有环
一种O(n)的办法就是(搞两个指针,一个每次递增一步,一个每次递增两步,
如果有环的话两者必然重合,反之亦然):bool check(const node* head)
*****************************************************************/
struct node {
char val;
node* next;
}
bool check(const node* head)
{
if(head==NULL)
return false;
node *low=head, *fast=head->next;
while(fast!=NULL && fast->next!=NULL)
{
low=low->next;
fast=fast->next->next;
if(low==fast)
return true;
}
return false;
}
Strcpy函数
char * strcpy( char *strDest, const char *strSrc )
{
assert( (strDest != NULL) &&(strSrc != NULL) );
char *address = strDest;
while( (*strDest++ = * strSrc++) != “\0‟ );
return address;
}
int atoi(char *s)
int atoi_test(const char *str)
{
int n= 0;
while(str != NULL)
{
int i=(int)(*str);
int zero = (int)'0';
int nine = (int)'9';
if(i>=zero && i<=nine)
{
n = 10*n + (i-zero);
}
else
{
break;
}
str++;
}
return n;
}
Const
int s[3] = {0,1,2};
//sd指向的内容是常量,sd可修改,*sd不可修改
//int const *sd;
const int const * sd;
sd = s;
sd = sd++;
//*sd = 3; //error
//st是常量,定义时候必须初始化,st不可修改,*st可修改
int * const st = s;
*st = 3;
//st = st++; //error
printf("sd = %d st = %d \n", *sd, *st);
关键字volatile
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
进程和线程的区别
什么是进程(Process):普通的解释就是,进程是程序的一次执行,而什么是线程(Thread),线程可以理解为进程中的执行的一段程序片段。在一个多任务环境中下面的概念可以帮助我们理解两者间的差别:
进程间是独立的,这表现在内存空间,上下文环境;线程运行在进程空间内。 一般来讲(不使用特殊技术)进程是无法突破进程边界存取其他进程内的存储空间;而线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间。 同一进程中的两段代码不能够同时执行,除非引入线程。线程是属于进程的,当进程退出时该进程所产生的线程都会被强制退出并清除。线程占用的资源要少于进程所占用的资源。 进程和线程都可以有优先级。在线程系统中进程也是一个线程。可以将进程理解为一个程序的第一个线程。
线程是指进程内的一个执行单元,也是进程内的可调度实体.与进程的区别:
(1)地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间;
(2)进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
(3)线程是处理器调度的基本单位,但进程不是.
(4)二者均可并发执行.
生命周期
多线程
Activity启动过程
不同分辨率屏幕如何进行适配?pd/px/sp 原理?
在设计之初,Android系统就被设计为一个可以在多种不同分辨率的设备上运行的操作系统。对于应用程序来说,系统平台向它们提供的是一个稳定的,跨平台的运行环境,而关于如何将程序以正确的方式显示到它所运行的平台上所需要的大部分技术细节,都由系统本身进行了处理,无需程序的干预。当然,系统本身也为程序提供了一系列API,所以在目标平台的分辨率是可以完全确定的情况下,程序也可以精确的控制自身在目标平台上的界面显示方式。 这个文档会说明系统平台究竟提供了哪些分辨率支持特性,与它们如何在程序中使用的信息。如果你遵循文档中列出的方法,就很容易让你的程序在所有支持的分辨率下都能完美显示。这样你就可以用一个单独的.apk文件,将你的程序发布到所有的平台上。 如果你已经发布过针对Android 1.5或更早版本平台的程序,你应该仔细阅读这篇文档,然后考虑一下到底如何让自己的老程序可以在拥有各种不同分辨率,并且运行着Android 1.6或更新平台上正常显示。在绝大部分情况下,只需要对程序作出小小的修改就可以达到目的,但你仍然需要尽可能地在各种分辨率的平台上进行测试。 术语和概念 屏幕长宽比 分辨率 密度 密度无关的像素(DIP)
低密度(120),ldpi
·WVGA(480×800),4.8~5.5寸
|
子线程如何创建Handler
Adapter如何实现
Activity之间如何传递数据
Ø 基于消息的通信机制 Intent--------boudle, extra 用这种简单的形式,一般而言传递一些简单的类型是比较容易的,如int、string等 详细介绍下Intent机制 Intent包含两部分: 1 目的【action】-------要去到哪里去 2 内容【category、data】----------路上带些什么,区分性数据和内容性数据 简单数据传递: Intent intent = new Intent(LoginActivity.this, MainActivity.class); intent.putExtra("flag", flag); startActivity(intent); //////////////////////////////////////////////////////////////////////////////////////////////// ///////////////// String flag = " "; Intent intent1 = this.getIntent(); flag = intent1.getStringExtra("flag"); //////////////////////////////////////////////////////////////////////////////////////////////// /////////////////// 数据类型有限,遇到不可序列化的数据Bitmap,Inputstream,或者是LinkList链表等数据类型就不太好用了 Ø 利用static静态数据,public static成员变量 我们千万不要以为Davlik虚拟机的垃圾回收器会帮助我们回收不需要的内存垃圾。事实上,回收器并不可靠, 尤其是手机上,是更加的不可靠。 因此,除非我们要使自己的程序变得越来越糟糕,否则尽量远离static。 注:如果经常使用static的Bitmap、Drawable等变量。可能就会抛出一个在Android系统中非常著名的异常( 以前budget这个单词一直记不住什么意思,自从经常抛出这个异常后,这个单词终于烂熟于心了,) ERROR/AndroidRuntime(4958): Caused by: java.lang.OutOfMemoryError: bitmap size exceeds VM budget 以前budget这个单词一直记不住什么意思,自从经常抛出这个异常后,这个单词终于烂熟于心了,) Ø 基于外部存储的传输 ,File/Preference/Sqlite,如果要针对第三方应用需要Content provider 作为一个完成的应用程序,数据存储操作是必不可少的。因此,Android系统一共提供了四种数据存储方式。 分别是:SharePreference、SQLite、Content Provider和File。由于Android系统中,数据基本都是私有的的,都是存放于“data/data/程序包名”目录下,所以要实现数据共享,正确方式是使用Content Provider。 Ø 基于Ipc的通信机制 context与service之间的传输,如Activity与Service之间的通信
Ø 基于Application Context 在一个activity初始化一个ArrayList<HashMap<Sting,Map>>对象,然后经过一个tableactivity,在传递到另 外一个activity,一开始直接考虑用putExtra,测试发现数据只能传递一次,就考虑用Application传递 package net.blogjava.mobile1; import android.app.Application; public class MyApp extends Application public Bitmap getBitmap() public void setBitmap(Bitmap bitmap) </application> 上面两段代码可以在任何的Service、Activity中使用。全局的
|
ListView如何优化
Service和activity通信
第一种:声明一个内部binder类,然后通过service的onbind()方法获取一个binder实例
public class MainService extends Service{
private String TAG = "MainService"; public ServiceBinder mBinder = new ServiceBinder(); /* 数据通信的桥梁 */
/* 重写Binder的onBind函数,返回派生类 */ @Override public IBinder onBind(Intent arg0) { return mBinder; }
@Override public void onCreate() { Toast.makeText( MainService.this, "Service Create...", Toast.LENGTH_SHORT).show(); super.onCreate(); }
@Override public int onStartCommand(Intent intent, int flags, int startId) { Toast.makeText(MainService.this, "Service StartCommand", Toast.LENGTH_SHORT).show(); return super.onStartCommand(intent, flags, startId); }
@Override public void onDestroy() { Toast.makeText( MainService.this, "Service Destroy", Toast.LENGTH_SHORT).show(); }
/* 第一种模式通信:Binder */ public class ServiceBinder extends Binder {
public void startDownload() throws InterruptedException { /* 模拟下载,休眠2秒 */ Toast.makeText( MainService.this, "模拟下载2秒钟,开始下载...", Toast.LENGTH_SHORT).show(); Thread.sleep(2); Toast.makeText( MainService.this, "下载结束...", Toast.LENGTH_SHORT).show(); } } } |
第二种:Service通过BroadCast广播与Activity通信
- Android 面试常见问题汇总
- Android WebView常见问题解决方案汇总
- Android系统编译过程中常见问题汇总
- android 面试常见问题
- android常见问题汇总
- Android 一些环境常见问题处理汇总
- Android 常见问题解决汇总
- Android开发常见问题汇总
- Java/C#面试常见问题汇总
- 英文面试常见问题汇总
- Android WebView常见问题及解决方案汇总
- Android WebView常见问题及解决方案汇总 .
- Android面试中的一个常见问题:Layout_weight的用法
- Android TextView 常见问题汇总
- Android开发面试经——1.常见人事面试问题
- Android WebView常见问题及解决方案汇总
- Android系统编译过程中常见问题汇总(1)
- Android刷机常见问题汇总
- Android WebView常见问题及解决方案汇总
- 面试常见问题知识点汇总(干货)