2017.8.15面试(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SiwenYY/article/details/77586840
2017.8.15面试(一)后,继续讲讲剩下的面试问题。

6、即时通讯聊天布局怎么实现?
6.1 XMPP(Extensible Messageing and Presence Protocol:可扩展消息与存在协议)是目前主流的四种IM(IM:instant messaging,即时消息)协议之一,其他三种分别为:即时信息和空间协议(IMPP)、空间和即时信息协议(PRIM)、针对即时通讯和空间平衡扩充的进程开始协议SIP(SIMPLE)。
6.2 即时通讯原理:

即时通讯(Instant Messenger,简称IM):是一种使人们能在网上识别在线用户并与他们实时交换消息的技术,是电子邮件发明以来迅速崛起的在线通讯方式。

IM完全基于TCP/IP和UDP进行通讯的,TCP/IP和UDP都是建立在更低层的IP协议上的两种通讯传输协议。前者是以数据流的形式,将传输数据经分割、打包后,通过两台机器之间建立起的虚电路,进行连续的、双向的、严格保证数据正确性的文件传输协议。而后者是以数据报的形式,对拆分后的数据的先后到达顺序不做要求的文件传输协议。

QQ就是使用UDP协议进行发送和接收消息的。当你的机器安装了OICQ以后,实际上,你既是服务端(Server),又是客户端(Client)。当你登录OICQ时,你的OICQ作为Client连接到腾讯公司的主服务器上,当你“看谁在线时,你的OICQ又一次作为Client从QQ Server上读取在线网友名单。当你和你的OICQ伙伴进行聊天时,如果你和对方的连接比较稳定,你和他的聊天内容都是以UDP的形式,在计算机之间传送。如果你和对方的连接不是很稳定,QQ服务器将为你们的聊天内容进行中转。其他的即时通信软件原理与此大同小异。

一般的步骤:

首先,用户A输入自己的用户名和密码登录即时通讯服务器,服务器通过读取用户数据库来验证用户身份,如果用户名、密码都正确,就登记用户A的IP地址、IM客户端软件的版本号及使用的TCP/UDP端口号,然后返回用户A登录成功的标志,此时用户A在 IM系统中的状态为在线(Online Presence)。

其次,根据用户A存储在IM服务器上的好友列表(Buddy List),服务器将用户A在线的相关信息发送到也同时在线的即时通讯好友的PC机,这些信息包括在线状态、IP地址、 IM客户端使用的TCP端口(Port)号等,即时通讯好友PC机上的即时通讯软件收到此信息后将在PC桌面上弹出一个小窗口予以提示。

第三步,即时通讯服务器把用户A存储在服务器上的好友列表及相关信息回送到他的PC机,这些信息包括也在线状态、IP地址、IM客户端使用的TCP端口(Port)号等信息,用户A的PC机上的IM客户端收到后将显示这些好友列表及其在线状态。

接下来,如果用户A想与他的在线好友用户B聊天,他将直接通过服务器发送过来的用户B的IP地址、TCP端口号等信息,直接向用户B的PC机发出聊天信息,用户B的IM客户端软件收到后显示在屏幕上,然后用户B再直接回复到用户A的PC机,这样双方的即时文字消息就不通过 IM服务器中转,而是通过网络进行点对点的直接通讯,这称为对等通讯方式(Peer To Peer)。在商用即时通讯系统中,如果用户A与用户B的点对点通讯由于防火墙、网络速度等原因难以建立或者速度很慢, IM服务器还提供消息中转服务,即用户A和用户B的即时消息全部先发送到IM服务器,再由服务器转发给对方。早期的IM系统,在IM客户端和IM服务器之间通讯采用采用UDP协议,UDP协议是不可靠的传输协议,而在 IM客户端之间的直接通讯中,采用具备可靠传输能力的TCP协议。随着用户需求和技术环境的发展,目前主流的即时通讯系统倾向于在即时通讯客户端之间、即时通讯客户端和即时通讯服务器之间都采用TCP协议。

6.3  聊天布局实现

聊天界面其实就是一个复杂的ListView,在数据适配器中,从application中获取到当前登录的用户,与聊天消息中的from参数进行比较,如果两者相同则说明是自己发送的消息,那么ListView中就显示发送消息的布局,否则显示接收消息的布局。适配器的代码如下:

       
       
public class ChartMessageAdapter extends ArrayAdapter<QQMessage> {
ImApp app;
public ChartMessageAdapter(Context context, List<QQMessage> objects) {
super(context, 0, objects);
Activity activity = (Activity) context;
app = (ImApp) activity.getApplication();
}
/**
* 根据集合中的position位置,返回不同的值,代表不同的布局。
*
*/
@Override
public int getItemViewType(int position) {
QQMessage msg = getItem(position);
// 消息来自谁,如果消息来自我自己,说明是我发送的
if (msg.from == app.getMyAccount()) {
// 代表自己发送的消息
return 0;
} else {
//代表接收到的消息
return 1;
}
}
/**
* 两种布局
*/
@Override
public int getViewTypeCount() {
return 2;
}
class ViewHolder {
TextView time;
TextView content;
ImageView head;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int type = getItemViewType(position);
if (0 == type) {
// 发送的布局
ViewHolder holder;
if (convertView == null) {
convertView = View.inflate(getContext(),
R.layout.item_chat_send, null);
holder = new ViewHolder();
holder.time = (TextView) convertView.findViewById(R.id.time);
holder.content = (TextView) convertView
.findViewById(R.id.content);
holder.head = (ImageView) convertView.findViewById(R.id.head);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// 设置值
QQMessage msg = getItem(position);
holder.time.setText(msg.sendTime);
holder.head.setImageResource(msg.fromAvatar);
holder.content.setText(msg.content);
return convertView;
} else {
// 接收的布局
ViewHolder holder;
if (convertView == null) {
convertView = View.inflate(getContext(),
R.layout.item_chat_receive, null);
holder = new ViewHolder();
holder.time = (TextView) convertView.findViewById(R.id.time);
holder.content = (TextView) convertView
.findViewById(R.id.content);
holder.head = (ImageView) convertView.findViewById(R.id.head);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// 设置值
QQMessage msg = getItem(position);
holder.head.setImageResource(msg.fromAvatar);
holder.time.setText(msg.sendTime);
holder.content.setText(msg.content);
return convertView;
}
}
}

关于QQ聊天界面更多详情可参考以下资料:

http://blog.csdn.net/baiyuliang2013/article/details/39551519  


7、listview条目如何实时刷新数据
对于ListView数据的刷新大家都知道,改变Adapter的数据源,然后调用Adapter的notifyDateSetChanged()方法即可。
如果每个item有图片的话,每个item的图片都需要重新加载,就算图片做了内存缓存,刷新一下图片也会闪一下,不停的刷新就会导致各个item的图片不停的闪,体验一点都不好。 针对某一个item进行局部更新,而不影响其它没有修改的item。
    
    
private void updateView(int itemIndex) {
//得到第一个可显示控件的位置,
int visiblePosition = mListView . getFirstVisiblePosition ();
//只有当要更新的view在可见的位置时才更新,不可见时,跳过不更新
if (itemIndex - visiblePosition >= 0) {
    //得到要更新的item的view
View view = mListView.getChildAt(itemIndex - visiblePosition);
//调用adapter更新界面
mAdapter.updateView(view, itemIndex);
}
}
更多详情可参考以下资料:

8、图片的放大缩小原理是什么?
图片放大缩小无非就是使用Matrix类,而这里通过手势控制,那自然是需要监听onTouch事件,所以原理简单来说,就是通过监听onTouch的各种事件来控制Matrix类了。其中具体控制方式如下:


需要用到的技术 
1)Handle事件分发机制 
2)Matrix矩阵 
3)ScaleGuestureDetector:手势缩放 
4)GuestureDetector:手势移动 
5)postdelay + runnable:实现缩放动画 
6)根据矩阵Matrix获得x,y轴的缩放值 
7)放大后的图片宽度大于屏幕宽度时,父控件消费掉事件
    
    
if (rectF.width() > getWidth()) {
getParent().requestDisallowInterceptTouchEvent(true);
}
更多详情可参考以下资料:

9、项目中是如何加载图片的?
由于安卓中设备给每个应用分配的空间大小有限(16M), Bitmap本身就非常占用资源,我们在使用 ListView, GridView 和 ViewPager控件加载多张图片时, 很容易因oom异常应用crash。
首先我们知道获取图像的来源一般有从网络加载 ;从文件读取 ;从资源文件加载 。
针对这三种情况我们一般使用BitmapFactory的:decodeStream, decodeFile,decodeResource,这三个函数来获取到bitmap然后再调用ImageView的setImageBitmap函数进行展现。

9.1 为什么bitmap消耗了那么多的内存?
BitmapFactory的每个decode函数都会生成一个bitmap对象,用于存放解码后的图像,然后返回该引用。如果图像数据较大就会造成bitmap对象申请的内存较多,如果图像过多就会造成内存不够用自然就会出现oom现象。
9.2 项目中加载图片的方式
a.读取图片尺寸和类型, 为了避免内存溢出,最好在加载图片前都对尺寸做检查,除非保证源图片没有大图
b.加载缩小后的图片到内存
c.避免在UI线程上处理图片( 使用软引用避免AsyncTask的引用导致内存泄漏。判断是否为空,避免空指针异常
如果我们使用ListView和GridView这种重复利用子控件的UI控件时,如果使用上面的AsyncTask,当任务完成的时候,无法保证该子控件是否已经被重用了。此外,任务开始的顺序和完成的顺序也无法保证。 
解决方案是创建一个专门的 Drawable 子类来储存载入图片的任务引用。这样使BitmapDrawable,当任务完成的时候placeholder 中的图片就能在ImageView显示了。
d.使用第三方加载图片库
比如现在市面上比较成熟流行的图片加载库 ImageLoader,Picasso,Glide,Fresco 每个加载库各有千秋,可以根据需求选择自己适合的库,大大提升开发速度,减少oom异常的概率。
Glide :模仿了Picasso的API,而且在他的基础上加了很多的扩展(比如gif等支持)。他们两者最大的区别在于默认的设置不同,Glide默认的Bitmap格式是RGB_565 ,比Picasso默认的ARGB_8888格式的内存开销要小一半;Picasso缓存的是全尺寸的(只缓存一种),而Glide缓存的是跟ImageView尺寸相同的(即56*56和128*128是两个缓存)
常用的第三方图片加载库详情请见 http://blog.csdn.net/siwenyy/article/details/75212528   

10、自定义view的流程?


10.1 Measure 过程
1. 测量过程由上至下,在measure过程的最后,每个视图将存储自己的尺寸大小和测量规格。
2. measure过程会为一个View及其所有子节点的mMeasureWidth和mMeasuredHeight变量赋值, 该值可以通过getMeasuredWidth和getMeasuredHeight方法获得。
3. measure过程的核心方法:  measure() - onMeasure() - setMeasuredDimension().  
    setMeasuredDimension
是测量阶段的终极方法,在onMeasure()方法中调用,将计算得到的尺寸,传递给该方法,测量阶段结束。在自定义视图时,不需要关心系统复杂的Measure过程,只需要调用setMeasuredDimension()设置根据MeasureSpec计算得到的尺寸即可。同时,onMeasure()方法也必须调用setMeasuredDimension()方法来设置重新测量。

View的绘制工作在onMeasure方法中进行,通过这个方法可以指定该控件在屏幕上的大小,重写该方法时需要计算控件的实际大小,然后调用setMeasuredDimension(int, int)将确定尺寸数值设置为控件的实际大小。
onMeasure被调用的时候传入两个参数widthMeasureSpec和heightMeasureSpec。每个都是一个32位的int值它分成两个部分高两位为测量模式,低30位为测量的大小。
我们可以通过:
int mode = MeasureSpec.getMode(xxxxxxx)获取到模式,
int size = MeasureSpec.getSize(xxxxxxx)获取到尺寸数值。
测量模式有三种:
EXACTLY:精确模式,当我们将layout_height以及layout_width设置为具体数值时,或者设置为match_parent的时候,使用的是这种模式
AT_MOST:最大值模式,当控件的layout_height以及layout_width被设置为wrap_content属性的时候,使用的是这种模式,这种模式下控件的尺寸只要不超过父控件允许的最大尺寸即可。
UNSPECIFIED:这种情况我见得不是很多,基本上没有用到。
View 默认情况下支持EXACTLY模式,因此如果不重写onMeasure方法时,只能使用EXACTLY模式,所定义的View只能指定具体尺寸,或者是match_parent,而不能是wrap_content.如果需要支持wrap_content就必须重写onMeasure方法。

10.2 Layout 过程
1. 子视图的具体位置都是相对于父视图而言的。View的onLayout()方法为空实现,而ViewGroup的onLayout为abstract,因此,自定义的View要继承ViewGroup时,必须实现onLayout函数。
2. 在Layout过程中,子视图会调用getMeasuredWidth()和getMeasuredHeight()方法获取到measure过程得到mMeasuredWidth和mMeasuredHeight,作为自己的width和height。然后调用每一个子视图的layout(),来确定每个子视图在父视图中的位置。

10.3 Draw 过程
1. 所有视图最终都是调用View的draw方法进行绘制。 在自定义视图中, 也不应该复写该方法, 而是复写onDraw()方法进行绘制, 如果自定义的视图确实要复写该方法,先调用super.draw()完成系统的绘制,再进行自定义的绘制。
2.  onDraw()方法默认是空实现,自定义绘制过程需要复写方法,绘制自身的内容。
3. dispatchDraw()发起对子视图的绘制,在View中默认为空实现,ViewGroup复写了dispatchDraw()来对其子视图进行绘制。自定义的ViewGroup不应该对dispatchDraw()进行复写。作者:wusp

View的绘制:
我们知道自定义一个View需要继承自View并重写构造方法以及onDraw方法。
般我们在Android Studio创建一个View的时候,会要求复写构造方法,默认情况下会有三个构造函数:
     
     
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
 
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.CustomizeStyleRef);
}
 
public CustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}
一般用于提供给第二个构造方法使用的,在第二个构造方法中会传给第三个构造方法一个默认的style id

推荐博文:

11、圆形进度条间显示文字怎么实现?
自定义圆形进度条
11.1 通过动画实现
定义res/anim/loading_anim.xml
     
     
<?xml version="1.0" encoding="UTF-8"?>
<animation-list android:oneshot="false"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:duration="150" android:drawable="@drawable/loading_01" />
<item android:duration="150" android:drawable="@drawable/loading_02" />
<item android:duration="150" android:drawable="@drawable/loading_03" />
<item android:duration="150" android:drawable="@drawable/loading_04" />
<item android:duration="150" android:drawable="@drawable/loading_05" />
<item android:duration="150" android:drawable="@drawable/loading_06" />
<item android:duration="150" android:drawable="@drawable/loading_07" />
</animation-list>
在layout文件中引用
     
     
<ProgressBar
android:id="@+id/progressBar1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="20dip"
android:layout_marginTop="20dip"
android:indeterminate="false"
android:indeterminateDrawable="@anim/loading_anim" />
11.2 通过自定义颜色实现
定义res/drawable/loading_color.xml
      
      
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360" >
<shape
android:innerRadiusRatio="3"
android:shape="ring"
android:thicknessRatio="8"
android:useLevel="false" >
<gradient
android:centerColor="#FFDC35"
android:centerY="0.50"
android:endColor="#CE0000"
android:startColor="#FFFFFF"
android:type="sweep"
android:useLevel="false" />
</shape>
</rotate>
在layout文件中引用
      
      
<ProgressBar
android:id="@+id/progressBar2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="false"
android:indeterminateDrawable="@drawable/loading_color" />
11.3 使用一张图片进行自定义
定义res/drawable/loading_img.xml
      
      
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<rotate
android:drawable="@drawable/exchange_loading"
android:fromDegrees="0.0"
android:pivotX="50.0%"
android:pivotY="50.0%"
android:toDegrees="360.0" />
</item>
</layer-list>
在layout文件中引用
      
      
<ProgressBar
android:id="@+id/progressBar3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="false"
android:indeterminateDrawable="@drawable/lodaing_img"
/>

a. 创建View
(1)自定义view属性,我们在res/values下面新建一个attr.xml文件,设置我们的自定义view属性
        
        
<!--?xml version="1.0" encoding="utf-8"?-->
<resources>
<declare-styleable name="CircleProgress">
<!-- 默认圆实心的颜色 -->
</ attr ></ attr >
<!-- 默认圆边框的颜色 -->
<!-- 默认圆边框的宽度 -->
<!-- 默认圆的半径 -->
<!-- 进度条的颜色 -->
<!-- 进度条的宽度 -->
<!-- 小圆的实心颜色 -->
<!-- 小圆的边框颜色 -->
<!-- 小圆的边框宽度 -->
<!-- 小圆的半径 -->
<!-- 文字的颜色 -->
<!-- 文字的字体大小 -->
</declare-styleable>
</resources>
(2)在我们的自定义View类中去获取这些属性
        
        
public CircleProgress(Context context) {
this(context,null);
}
public CircleProgress(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CircleProgress(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircleProgress);
              .........
}
设置画笔的方法,new画笔的操作不要在onDraw()方法中进行
         
         
private void setPaint() {
//默认圆
defaultCriclePaint = new Paint();
defaultCriclePaint.setAntiAlias(true);//抗锯齿
defaultCriclePaint.setDither(true);//防抖动
defaultCriclePaint.setStyle(Paint.Style.STROKE);
defaultCriclePaint.setStrokeWidth(defaultCircleStrokeWidth);
defaultCriclePaint.setColor(defaultCircleStrokeColor);//这里先画边框的颜色,后续再添加画笔画实心的颜色
//默认圆上面的进度弧度
progressPaint = new Paint();
progressPaint.setAntiAlias(true);
progressPaint.setDither(true);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeWidth(progressWidth);
progressPaint.setColor(progressColor);
progressPaint.setStrokeCap(Paint.Cap.ROUND);//设置画笔笔刷样式
//进度上面的小圆
smallCirclePaint = new Paint();
smallCirclePaint.setAntiAlias(true);
smallCirclePaint.setDither(true);
smallCirclePaint.setStyle(Paint.Style.STROKE);
smallCirclePaint.setStrokeWidth(smallCircleStrokeWidth);
smallCirclePaint.setColor(smallCircleStrokeColor);
//画进度上面的小圆的实心画笔(主要是将小圆的实心颜色设置成白色)
smallCircleSolidePaint = new Paint();
smallCircleSolidePaint.setAntiAlias(true);
smallCircleSolidePaint.setDither(true);
smallCircleSolidePaint.setStyle(Paint.Style.FILL);
smallCircleSolidePaint.setColor(smallCircleSolideColor);
//文字画笔
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setDither(true);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setColor(textColor);
textPaint.setTextSize(textSize);
}

b.处理View的布局( onMeasure
        
        
    // 如果该View布局的宽高开发者没有精确的告诉,则需要进行测量,如果给出了精确的宽高则我们就
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize;
int heightSize;
int strokeWidth = Math.max(defaultCircleStrokeWidth, progressWidth);
if(widthMode != MeasureSpec.EXACTLY){
widthSize = getPaddingLeft() + defaultCircleRadius*2 + strokeWidth + getPaddingRight();
widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
}
if(heightMode != MeasureSpec.EXACTLY){
heightSize = getPaddingTop() + defaultCircleRadius*2 + strokeWidth + getPaddingBottom();
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

c.绘制View( onDraw()
        
        
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
//画默认圆
canvas.drawCircle(defaultCircleRadius,defaultCircleRadius,defaultCircleRadius,defaultCriclePaint);
//画进度圆弧
currentAngle = getProgress()*1.0f/getMax()*360;
canvas.drawArc(new RectF(0,0,defaultCircleRadius*2,defaultCircleRadius*2),mStartSweepValue, currentAngle ,false,progressPaint);
//画中间文字
String text = getProgress()+"%";
//获取文字的长度的方法
float textWidth = textPaint.measureText(text );
float textHeight = (textPaint.descent() + textPaint.ascent()) / 2;
canvas.drawText(text, defaultCircleRadius-textWidth/2, defaultCircleRadius-textHeight, textPaint);
canvas.restore();
}

d.与用户进行交互( 进度条圆弧以及中间的文字动起来
       
       
public class MainActivity extends AppCompatActivity {
private CircleProgress circleProgress;
private int progress;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case HANDLER_MESSAGE:
progress = circleProgress.getProgress();
circleProgress.setProgress(++progress);
if(progress >= 100){
handler.removeMessages(HANDLER_MESSAGE);
progress = 0;
circleProgress.setProgress(0);
}else{
handler.sendEmptyMessageDelayed(HANDLER_MESSAGE, 100);
}
break;
}
}
};
public static final int HANDLER_MESSAGE = 2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
circleProgress = (CircleProgress) findViewById(R.id.circleProgress);
circleProgress.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Message message = Message.obtain();
message.what = HANDLER_MESSAGE;
handler.sendMessage(message);
}
});
}
}


12、socket的理解 


猜你喜欢

转载自blog.csdn.net/SiwenYY/article/details/77586840