经历过面试的人应该都知道,一般我们在进行技术面试的时候,面试官都会问你,在项目开发中遇到过什么棘手的问题?最后是怎么解决的?本人之前就问到过好几次,可是由于准备不足,一时之间想不起来所遇到过的“棘手”问题,所以好几次都没能很好地回答这个问题。因此,在这里记录下本人在接下来工作中遇到的棘手问题以及最后解决方案。
1. 通过SharePerference初始化数据(数据量比较大)时,造成ANR(Application Not Response)
问题描述:
第一次启动应用,考虑到初始化的数据量过多,直接在UI线程里操作可能会阻塞UI线程造成ANR,所以使用了异步方式(线程)完成数据初始化操作,然后通过Handler通知主线程继续往下运行,然而实际运行时还是造成了ANR,经调试结合debug信息发现,初始化早已完成,但主线程却没有接收到Handler发出的完成信息继续往下执行,如果没有其他原因,即使他在主线程中执行完成它也不应该也不足以导致ANR,然而实际却出现了。
PS:打断点debug时又正常了(╬◣д◢)
解决方案
通查看ANR的trace日志,终于找到了发生ANR的地方
可以看到是卡在了QueueWork.waitToFinish,该函数的等待导致发生了ANR
看日志中描述的waitToFinish()方法:
/**
* Trigger queued work to be processed immediately. The queued work is processed on a separate
* thread asynchronous. While doing that run and process all finishers on this thread. The
* finishers can be implemented in a way to check weather the queued work is finished.
*
* Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
* after Service command handling, etc. (so async work is never lost)
*/
public static void waitToFinish() {
...
}
看上面方法解释:当Activity调用onPause()方法,或者BroadcastReceiver调用onReceive()方法的时候会调用该方法,这样就能保证异步任务不会丢失。
在结合调用的apply方法,找到对应代码出问题地方
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
可以看到apply方法会将等待写入到文件系统的任务放在QueuedWork的等待完成队列里。所以如果我们使用SharedPreference的apply方法, 虽然该方法可以很快返回, 并在其它线程里将键值对写入到文件系统, 但是当Activity的onPause等方法被调用时,会等待写入到文件系统的任务完成,所以如果写入比较慢,主线程就会出现ANR问题。
再看commit()方法
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
它会在调用线程就等待写入任务完成,所以不会将等待的时间转嫁到主线程
apply方法和commit方法对比:
为了避免出现ANR问题,最好还是别使用apply操作,用commit方法最保险。如果担心在主线程调用commit方法会出现ANR,可以将所有的commit任务放到单线程池的线程里去执行。
测试在测试app时反馈一个问题,说App被杀进程后再次启动时变得很慢了,于是在我自己手机上也测试了一下,发现并不存在这样的问题,启动速度非常正常,因此首先怀疑是网络的问题。试着把他的手机切换到WiFi网络状态,果然启动变正常了,然后切换到他的4G网络,又变慢了。但是同样的4G网络,他启动其他app速度是很快的,正常的,而且看高清视频也完全没有问题,那就说明不是网络信号不良的问题,这就非常奇怪了。
经过网上查阅资料,发现都指向了电信卡的问题,问过测试,果然,他用的也是电信的4G网络,然后又试着切换到了移动的4G网络,果然启动速度正常了( 晕,这是什么鬼问题??? )
继续百度寻找答案,发现都指向了DNS解析设置的问题,即ipv4、ipv6解析的问题,android默认不支持ipv6解析。
android为何不支持ipv6解析?
ipv4地址即将枯竭,但是ipv6依然迟迟无法普及,原因之一就是有些巨头不提供支持。
不过在Google开发者、IPv6权威专家Lorenzo Colliti看来,Android不支持IPv6也是有苦衷的,比如会影响那些依赖IPv4的应用,无法强制开发者采用IPv6网络地址,地址转换后性能会有损失等等。
问题排查
使用以下代码 DNS 解析的 IP 地址
try {
InetAddress[] mInetAddresses= InetAddress.getAllByName("xxxx.cn");
for(InetAddress address: mInetAddresses){
System.out.println(address.getHostAddress());
}} catch (UnknownHostException e) {
e.printStackTrace();}
发现
1、连接到公司wifi,只解析到 ipv4 地址
2、连接到4G网,解析到了ipv4、ipv6俩个地址但是ipv6默认为集合中的第一个,是否我们可以尝试修改集合第一个为ipv4呢?
解决方案
自定义okhttp中dns解析
查看了下okhttp开放了自定义dns方法,于是追踪到源码可以看到
package okhttp3;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
public interface Dns {
Dns SYSTEM = new Dns() {
public List lookup(String hostname) throws UnknownHostException {
if (hostname == null) {
throw new UnknownHostException("hostname == null");
} else {
return Arrays.asList(InetAddress.getAllByName(hostname));
}
}
};
List lookup(String var1) throws UnknownHostException;
}
于是我们去调换集合中ipv4 ipv6位置,将ipv4当到集合首位
package cn.finalteam.okhttpfinal.https;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import okhttp3.Dns;
public class ApiDns implements Dns {
@Override
public List lookup(String hostname) throws UnknownHostException {
if (hostname == null) {
throw new UnknownHostException("hostname == null");
} else {
try {
List mInetAddressesList = new ArrayList<>();
InetAddress[] mInetAddresses = InetAddress.getAllByName(hostname);
for (InetAddress address : mInetAddresses) {
if (address instanceof Inet4Address) {
mInetAddressesList.add(0, address);
} else {
mInetAddressesList.add(address);
}
}
return mInetAddressesList;
} catch (NullPointerException var4) {
UnknownHostException unknownHostException = new UnknownHostException("Broken system behaviour");
unknownHostException.initCause(var4);
throw unknownHostException;
}
}
}
}
将自定义方法插入到okhttp中
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(timeout, TimeUnit.MILLISECONDS)
.writeTimeout(timeout, TimeUnit.MILLISECONDS)
.readTimeout(timeout, TimeUnit.MILLISECONDS)
.dns(new ApiDns());
最后在打包,运行,切换电信4G网络,果然应用启动正常了
这个问题困扰了我好长一段时间,因为之前查找原因时在网上看到有网友反馈华为手机在升级到安卓9.0系统后,好多应用启动速度都变慢了,而同事的手机刚好也是华为手机,而且也是差不多在升级了9.0系统后出现了这个问题,因此想着不是我应用代码的问题,同时期间还有其他开发任务,就没有过多花时间在这上面,今天着重花了点时间来解决,终于在网上找到了相关问题解决方案的大神的文章Android部分手机4G网第一次请求很慢(wifi正常)解决方案至此,问题终于解决。
APP在被杀进程后再次启动(或第一次启动)时,由于Application里面做了较多的一些列初始化工作,导致启动APP后没有第一时间展示启动页面,取而代之的是2秒左右的白屏,之后才展示启动页面,这就导致不太好的用户体验。
Application的构造器方法——>attachBaseContext()——>onCreate()——>Activity的构造方法——>onCreate()——>配置主题中背景等属性——>onStart()——>onResume()——>测量布局绘制显示在界面上。
从你点击桌面的图标开始安卓系统会从Zygote进程中fork创建出一个新的进程分配给该应用,之后会依次创建和初始化Application类。当这些操作走完之后,你才能看到SplashActivity的第一眼。那么这期间你Application里onCreate做了N多的SDK的初始化,这段时间您的屏幕都是你手机默认主题的颜色(白色/黑色)。这就是为什么我们做的APP会出现白屏。
1. 新增一个Activity并将其作为启动页(这里命名为FastStartAppActivity)
2. 重写FastStartAPPActivity的onCreate()方法如下
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startActivity(new Intent(this, SplashActivity.class));//SplashActivity为原启动页Activity
overridePendingTransition(0, 0);
finish();
}
3. 指定并设置FastStartAppActivity的Theme,设置Activity背景
bg_splash为原启动页SplashActivity的背景图片,根据需求设置全屏与否
完成以上步骤再次运行,就可以发现白屏没有了。
参考资料:
SharedPreferences的apply和Commit方法的那些坑
Android部分手机4G网第一次请求很慢(wifi正常)解决方案
Android - 启动白屏分析与优化