}if (mIWebViewClient != null) {mIWebViewClient.onPageFinished(view, newProgress);}
}

}


可以看到,我们使用了`mProgressFinishThreshold`这个变量控制注入时机,这与前面提及的`当progress达到80的时候,加载出来的页面已经基本可用了`是相呼应的。> 达到80%很容易,达到100%却很难。正是因为这个原因,页面的进度加载到80%的时候,实际上dom树已经渲染得差不多了,表明WebView已经解析了<html>标签,这时候注入一定是成功的。在`WebViewClient.onProgressChanged()`实现js注入有几个需要注意的地方:1.  上文提到的多次注入控制,我们使用了mCallProgressCallback变量控制
2.  重新加载一个URL之前,需要重置mCallProgressCallback,让重新加载后的页面再次注入js
3.  注入的进度阈值可以自由定制,理论上10%-100%都是合理的,我们使用了80%。H5页面、Weex页面与Native页面交互——KaolaRouter
-----------------------------------H5页面、Weex页面与Native页面的交互是通过URL拦截实现的。在WebView中,`WebViewClient.shouldOverrideUrlLoading()`方法能够获取到当前加载的URL,然后把URL传递给考拉路由框架,便可以判断URL是否能够跳转到其他非H5页面,考拉路由框架在[《考拉Android客户端路由总线设计》]( )一文中有详细介绍,但当时未引入Weex页面,关于如何整合三者的通信,后续文章会有详细介绍。在`WebViewClient.shouldOverrideUrlLoading()`中,根据URL类型做了判断:

public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (StringUtils.isNotBlank(url) && url.equals(“about:blank”)) { //js调用reload刷新页面时候,个别机型跳到空页面问题修复
url = getUrl();
}
url = WebViewUtils.removeBlank(url);
mCallProgressCallback = true;
//允许启动第三方应用客户端
if (WebViewUtils.canHandleUrl(url)) {
boolean handleByCaller = false;
// 如果不是用户触发的操作,就没有必要交给上层处理了,直接走url拦截规则。
if (null != mIWebViewClient && isTouchByUser()) {
// 先交给业务层拦截处理
handleByCaller = mIWebViewClient.shouldOverrideUrlLoading(view, url);
}
if (!handleByCaller) {
// 业务层不拦截,走通用路由总线规则
handleByCaller = handleOverrideUrl(url);
}
mRedirectProtected = true;
return handleByCaller || super.shouldOverrideUrlLoading(view, url);
} else {
try {
notifyBeforeLoadUrl(url);
// https://sumile.cn/archives/1223.html
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
intent.setSelector(null);
mContext.startActivity(intent);
if (!mIsBlankPageRedirect) {
back();
}
} catch (Exception e) {
ExceptionUtils.printExceptionTrace(e);
}
return true;
}
}

private boolean handleOverrideUrl(final String url) {
RouterResult result = WebActivityRouter.startFromWeb(
new IntentBuilder(mContext, url).setRouterActivityResult(new RouterActivityResult() {
@Override
public void onActivityFound() {
if (!mIsBlankPageRedirect) {
// 路由拦截成功以后,为防止首次进入WebView产生白屏,因此加了保护机制
back();
}
}

            @Overridepublic void onActivityNotFound() {}}));
return result.isSuccess();

}


代码里写了注释,就不一一解释了。WebView下拉刷新实现
-------------由于考拉使用的下拉刷新跟Material Design所使用的下拉刷新样式不一致,因此不能直接套用`SwipeRefreshLayout`。考拉使用的是一套改造过的[Android-PullToRefresh]( ),WebView的下拉刷新,正是继承自`PullToRefreshBase`来实现的。

/**

  • 创建者:Square Xu

  • 日期:2017/2/23

  • 功能模块:webview下拉刷新组件
    */
    public class PullToRefreshWebView extends PullToRefreshBase {
    public PullToRefreshWebView(Context context) {
    super(context);
    }

    public PullToRefreshWebView(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

    public PullToRefreshWebView(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs);
    }

    public PullToRefreshWebView(Context context, Mode mode) {
    super(context, mode);
    }

    public PullToRefreshWebView(Context context, Mode mode, AnimationStyle animStyle) {
    super(context, mode, animStyle);
    }

    @Override
    public Orientation getPullToRefreshScrollDirection() {
    return Orientation.VERTICAL;
    }

    @Override
    protected KaolaWebview createRefreshableView(Context context, AttributeSet attrs) {
    KaolaWebview kaolaWebview = new KaolaWebview(context, attrs);
    //解决键盘弹起时候闪动的问题
    setGravity(AXIS_PULL_BEFORE);
    return kaolaWebview;
    }

    @Override
    protected boolean isReadyForPullEnd() {
    return false;
    }

    @Override
    protected boolean isReadyForPullStart() {
    return getRefreshableView().getScrollY() == 0;
    }
    }


考拉使用了全屏模式实现沉浸式状态栏及[滑动返回]( ),全屏模式和WebView下拉刷新相结合对键盘的弹起产生了闪动效果,经过组内大神的研究与多次调试(感谢@俊俊),发现`setGravity(AXIS_PULL_BEFORE)`能够解决闪动的问题。如何处理加载错误(Http、SSL、Resource)?
----------------------------对于WebView加载一个网页过程中所产生的错误回调,大致有三种:*   `WebViewClient.onReceivedHttpError(webView, webResourceRequest, webResourceResponse)`任何HTTP请求产生的错误都会回调这个方法,包括主页面的html文档请求,iframe、图片等资源请求。在这个回调中,由于混杂了很多请求,不适合用来展示加载错误的页面,而适合做监控报警。当某个URL,或者某个资源收到大量报警时,说明页面或资源可能存在问题,这时候可以让相关运营及时响应修改。*   `WebViewClient.onReceivedSslError(webview, sslErrorHandler, sslError)`任何HTTPS请求,遇到SSL错误时都会回调这个方法。比较正确的做法是让用户选择是否信任这个网站,这时候可以弹出信任选择框供用户选择(大部分正规浏览器是这么做的)。但人都是有私心的,何况是遇到自家的网站时。我们可以让一些特定的网站,不管其证书是否存在问题,都让用户信任它。在这一点上,分享一个小坑。考拉的SSL证书使用的是GeoTrust的`GeoTrust SSL CA - G3`,但是在某些机型上,打开考拉的页面都会提示证书错误。这时候就不得不使用“绝招”——让考拉的所有二级域都是可信任的。

@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if (UrlUtils.isKaolaHost(getUrl())) {
handler.proceed();
} else {
super.onReceivedSslError(view, handler, error);
}
}


*   `WebViewClient.onReceivedError(webView, webResourceRequest, webResourceError)`只有在主页面加载出现错误时,才会回调这个方法。这正是展示加载错误页面最合适的方法。然鹅,如果不管三七二十一直接展示错误页面的话,那很有可能会误判,给用户造成经常加载页面失败的错觉。由于不同的WebView实现可能不一样,所以我们首先需要排除几种误判的例子:1.  加载失败的url跟WebView里的url不是同一个url,排除;
2.  errorCode=-1,表明是ERROR\_UNKNOWN的错误,为了保证不误判,排除
3.  failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR\_BAD\_URL,排除

@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);

// -12 == EventHandle.ERROR_BAD_URL, a hide return code inside android.net.http package
if ((failingUrl != null && !failingUrl.equals(view.getUrl()) && !failingUrl.equals(view.getOriginalUrl())) /* not subresource error*/|| (failingUrl == null && errorCode != -12) /*not bad url*/|| errorCode == -1) { //当 errorCode = -1 且错误信息为 net::ERR_CACHE_MISSreturn;
}if (!TextUtils.isEmpty(failingUrl)) {if (failingUrl.equals(view.getUrl())) {if (null != mIWebViewClient) {mIWebViewClient.onReceivedError(view);}}
}

}


如何操作cookie?
-----------Cookie默认情况下是不需要做处理的,如果有特殊需求,如针对某个页面设置额外的Cookie字段,可以通过代码来控制。下面列出几个有用的接口:*   获取某个url下的所有Cookie:`CookieManager.getInstance().getCookie(url)`
*   判断WebView是否接受Cookie:`CookieManager.getInstance().acceptCookie()`
*   清除Session Cookie:`CookieManager.getInstance().removeSessionCookies(ValueCallback<Boolean> callback)`
*   清除所有Cookie:`CookieManager.getInstance().removeAllCookies(ValueCallback<Boolean> callback)`
*   Cookie持久化:`CookieManager.getInstance().flush()`
*   针对某个主机设置Cookie:`CookieManager.getInstance().setCookie(String url, String value)`如何调试WebView加载的页面?
-----------------在Android 4.4版本以后,可以使用Chrome开发者工具调试WebView内容[^5]( )。调试需要在代码里设置打开调试开关。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}


开启后,使用USB连接电脑,加载URL时,打开Chrome开发者工具,在浏览器输入

chrome://inspect


可以看到当前正在浏览的页面,点击`inspect`即可看到WebView加载的内容。WebView优化
=========除了上面提到的基本操作用来实现一个完整的浏览器功能外,WebView的加载速度、稳定性和安全性是可以进一步加强和提高的。下面从几个方面介绍一下WebView的优化方案,这些方案可能并不是都适用于所有场景,但思路是可以借鉴的。CandyWebCache
-------------我们知道,在加载页面的过程中,js、css和图片资源占用了大量的流量,如果这些资源一开始就放在本地,或者只需要下载一次,后面重复利用,岂不美哉。尽管WebView也有几套缓存方案[^6]( ),但是总体而言效果不理想。基于自建缓存系统的思路,由网易杭研研发的CandyWebCache项目应运而生。CandyWebCache是一套支持离线缓存WebView资源并实时更新远程资源的解决方案,支持打母包时下载当前最新的资源文件集成到apk中,也支持在线实时更新资源。在WebView中,我们需要拦截`WebViewClient.shouldInterceptRequest()`方法,检测缓存是否存在,存在则直接取本地缓存数据,减少网络请求产生的流量。

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (WebSwitchManager.isWebCacheEnabled()) {
try {
WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, request);
return WebViewUtils.handleResponseHeader(resourceResponse);
} catch (Throwable e) {
ExceptionUtils.uploadCatchedException(e);
}
}
return super.shouldInterceptRequest(view, request);
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (WebSwitchManager.isWebCacheEnabled()) {
try {
WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, url);
return WebViewUtils.handleResponseHeader(resourceResponse);
} catch (Throwable e) {
ExceptionUtils.uploadCatchedException(e);
}
}
return super.shouldInterceptRequest(view, url);
}


除了上述缓存方案外,腾讯的QQ会员团队也推出了开源的解决方案[VasSonic]( ),旨在提升H5的页面访问体验,但最好由前后端一起配合改造。这套整体的解决方案有很多借鉴意义,考拉也在学习中。Https、HttpDns、CDN
-----------------将http请求切换为https请求,可以降低运营商网络劫持(js劫持、图片劫持等)的概率,特别是使用了http2后,能够大幅提升web性能,减少网络延迟,减少请求的流量。HttpDns,使用http协议向特定的DNS服务器进行域名解析请求,代替基于DNS协议向运营商的Local DNS发起解析请求,可以降低运营商DNS劫持带来的访问失败。目前在WebView上使用HttpDns尚存在一定问题,网上也没有较好的解决方案([阿里云Android WebView+HttpDns最佳实践]( )、[腾讯云HttpDns SDK接入]( )、[webview接入HttpDNS实践]( )),因此还在调研中。另一方面,可以把静态资源部署到多路CDN,直接通过CDN地址访问,减少网络延迟,多路CDN保障单个CDN大面积节点访问失败时可切换到备用的CDN上。WebView独立进程
-----------WebView实例在Android7.0系统以后,已经可以选择运行在一个独立进程上[^7]( );8.0以后默认就是运行在独立的沙盒进程中[^8]( ),未来Google也在朝这个方向发展,具体的WebView历史可以参考上一篇文章[《如何设计一个优雅健壮的Android WebView?(上)》]( )第一小节。Android7.0系统以后,WebView相对来说是比较稳定的,无论承载WebView的容器是否在主进程,都不需要担心WebView崩溃导致应用也跟着崩溃。然后7.0以下的系统就没有这么幸运了,特别是低版本的WebView。考虑应用的稳定性,我们可以把7.0以下系统的WebView使用一个独立进程的Activity来包装,这样即使WebView崩溃了,也只是WebView所在的进程发生了崩溃,主进程还是不受影响的。

public static Intent getWebViewIntent(Context context) {
Intent intent;
if (isWebInMainProcess()) {
intent = new Intent(context, MainWebviewActivity.class);
} else {
intent = new Intent(context, WebviewActivity.class);
}
return intent;
}

public static boolean isWebInMainProcess() {
return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N;
}


WebView免流
---------从去年开始,市场上出现了一批互联网套餐卡,如腾讯王卡、蚂蚁宝卡、京东强卡、阿里鱼卡、网易白金卡等,这些互联网套餐相比传统的运营商套餐来说,资费便宜,流量多,甚至某些卡还拥有特殊权限——对某些应用免流。如[网易白金卡]( ),对于网易系与百度系的部分应用实现免流。### 免流原理市面上常见的免流应用,原理无非就是走“特殊通道”,让这一部分的流量不计入运营商的流量统计平台中。Android中要实现这种“特殊通道”,有几种方案。1.  微屁恩。目前运营商貌似没有采用这种方案,但确实是可行的。由于国情,不多介绍,懂的自然懂。
2.  全局代理。把所有的流量中转到代理服务器中,代理服务器再根据流量判断是否属于免流流量。
3.  IP直连。走这个IP的所有流量,服务器判断是否免流。### WebView免流方案对于上面提到的几种方案,native页面所有的请求都是应用层发起的,实际上都比较好实现,但WebView的页面和资源请求是通过JNI发起的,想要拦截请求的话,需要一些功夫。网罗网上的所有方案,目前觉得可行的有两种,分别是全局代理和拦截`WebViewClient.shouldInterceptRequest()`。#### 全局代理由于WebView并没有提供接口针对具体的WebView实例设置代理,所以我们只能进行全局代理。设置全局代理时,需要通知系统代理环境发生了改变,不幸地是,Android并没有提供公开的接口,这就导致了我们只能hook系统接口,根据不同的系统版本来实现通知的目的[^9]( )、[^10]( )。6.0以后的系统,尚未尝试是否可行,根据公司同事的反馈,和5.0系统的方案是一致的。

/**

  • Set Proxy for Android 4.1 - 4.3.
    */
    @SuppressWarnings(“all”)
    private static boolean setProxyJB(WebView webview, String host, int port) {
    Log.d(LOG_TAG, “Setting proxy with 4.1 - 4.3 API.”);

    try {
    Class wvcClass = Class.forName(“android.webkit.WebViewClassic”);
    Class wvParams[] = new Class[1];
    wvParams[0] = Class.forName(“android.webkit.WebView”);
    Method fromWebView = wvcClass.getDeclaredMethod(“fromWebView”, wvParams);
    Object webViewClassic = fromWebView.invoke(null, webview);

     Class wv = Class.forName("android.webkit.WebViewClassic");Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore");Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webViewClassic);Class wvc = Class.forName("android.webkit.WebViewCore");Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame");Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance);Class bf = Class.forName("android.webkit.BrowserFrame");Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge");Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame);Class ppclass = Class.forName("android.net.ProxyProperties");Class pparams[] = new Class[3];pparams[0] = String.class;pparams[1] = int.class;pparams[2] = String.class;Constructor ppcont = ppclass.getConstructor(pparams);Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge");Class params[] = new Class[1];params[0] = Class.forName("android.net.ProxyProperties");Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params);updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null));
    

    } catch (Exception ex) {
    Log.e(LOG_TAG, "Setting proxy with >= 4.1 API failed with error: " + ex.getMessage());
    return false;
    }

    Log.d(LOG_TAG, “Setting proxy with 4.1 - 4.3 API successful!”);
    return true;
    }

/**

  • Set Proxy for Android 5.0.
    */
    public static void setWebViewProxyL(Context context, String host, int port) {
    System.setProperty(“http.proxyHost”, host);
    System.setProperty(“http.proxyPort”, port + “”);
    try {
    Context appContext = context.getApplicationContext();
    Class applictionClass = Class.forName(“android.app.Application”);
    Field mLoadedApkField = applictionClass.getDeclaredField(“mLoadedApk”);
    mLoadedApkField.setAccessible(true);
    Object mloadedApk = mLoadedApkField.get(appContext);
    Class loadedApkClass = Class.forName(“android.app.LoadedApk”);
    Field mReceiversField = loadedApkClass.getDeclaredField(“mReceivers”);
    mReceiversField.setAccessible(true);
    ArrayMap receivers = (ArrayMap) mReceiversField.get(mloadedApk);
    for (Object receiverMap : receivers.values()) {
    for (Object receiver : ((ArrayMap) receiverMap).keySet()) {
    Class clazz = receiver.getClass();
    if (clazz.getName().contains(“ProxyChangeListener”)) {
    Method onReceiveMethod = clazz.getDeclaredMethod(“onReceive”, Context.class, Intent.class);
    Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
    onReceiveMethod.invoke(receiver, appContext, intent);
    }
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

需要注意的是,在WebView退出时,需要重置代理。#### 拦截`WebViewClient.shouldInterceptRequest()`拦截`WebViewClient.shouldInterceptRequest()`的目的是使用免流的第三种方案——IP替换。直接看代码。

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, request);
if (request.getUrl() != null && request.getMethod().equalsIgnoreCase(“get”)) {
Uri uri = request.getUrl();
String url = uri.toString();
String scheme = uri.getScheme().trim();
String host = uri.getHost();
String path = uri.getPath();
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(host)) {
return null;
}
// HttpDns解析css文件的网络请求及图片请求
if ((scheme.equalsIgnoreCase(“http”) || scheme.equalsIgnoreCase(“https”)) && (path.endsWith(".css")
|| path.endsWith(".png")
|| path.endsWith(".jpg")
|| path.endsWith(".gif")
|| path.endsWith(".js"))) {
try {
URL oldUrl = new URL(uri.toString());
URLConnection connection;
// 获取HttpDns域名解析结果
List ips = HttpDnsManager.getInstance().getIPListByHostAsync(host);
if (!ListUtils.isEmpty(ips)) {
String ip = ips.get(0);
String newUrl = url.replaceFirst(oldUrl.getHost(), ip);
connection = new URL(newUrl).openConnection(); // 设置HTTP请求头Host域
connection.setRequestProperty(“Host”, oldUrl.getHost());
} else {
connection = new URL(url).openConnection(); // 设置HTTP请求头Host域
}
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(url);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
return new WebResourceResponse(mimeType, “UTF-8”, connection.getInputStream());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return super.shouldInterceptRequest(view, request);
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (!TextUtils.isEmpty(url) && Uri.parse(url).getScheme() != null) {
Uri uri = Uri.parse(url);
String scheme = uri.getScheme().trim();
String host = uri.getHost();
String path = uri.getPath();
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(host)) {
return null;
}
// HttpDns解析css文件的网络请求及图片请求
if ((scheme.equalsIgnoreCase(“http”) || scheme.equalsIgnoreCase(“https”)) && (path.endsWith(".css")
|| path.endsWith(".png")
|| path.endsWith(".jpg")
|| path.endsWith(".gif")
|| path.endsWith(".js"))) {
try {
URL oldUrl = new URL(uri.toString());
URLConnection connection;
// 获取HttpDns域名解析结果
List ips = HttpDnsManager.getInstance().getIPListByHostAsync(host);
if (!ListUtils.isEmpty(ips)) {
String ip = ips.get(0);
String newUrl = url.replaceFirst(oldUrl.getHost(), ip);
connection = new URL(newUrl).openConnection(); // 设置HTTP请求头Host域
connection.setRequestProperty(“Host”, oldUrl.getHost());
} else {
connection = new URL(url).openConnection(); // 设置HTTP请求头Host域
}
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(url);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
return new WebResourceResponse(mimeType, “UTF-8”, connection.getInputStream());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return super.shouldInterceptRequest(view, url);
}


使用此种方案,还可以把WebView网络请求与native网络请求使用的框架统一起来,方便管理。总结
==本文介绍了WebView在开发中的一些实践经验和优化流程。为了满足业务需求,WebView着实提供了非常丰富的接口供应用层处理业务逻辑。针对WebView的二次开发,本文介绍了一些常用的回调处理逻辑以及开发过程中总结下的经验。由于是经验,不一定是准确的,若有错误的地方,敬请指出纠正,不胜感激!* * *

如何设计一个优雅健壮的Android WebView?,吊打面试官系列相关推荐

  1. 如何设计一个优雅健壮的Android WebView?(上)

    原文链接 https://kaolamobile.github.io/2017/12/10/design-an-elegant-and-powerful-android-webview-part-on ...

  2. 如何设计一个优雅健壮的Android WebView?(上) 基于考拉电商平台的WebView实践

    前言 Android应用层的开发有几大模块,其中WebView是最重要的模块之一.网上能够搜索到的WebView资料可谓寥寥,Github上的开源项目也不是很多,更别提有一个现成封装好的WebView ...

  3. 如何设计一个优雅健壮的Android WebView?(下

    nativeReady = false; var jsoncommand = JSON.stringify(command); var _temp = prompt(jsoncommand,''); ...

  4. 我在华为做Android外包的真实经历!吊打面试官系列!

    导语 本部分内容是关于Android进阶的一些知识总结,涉及到的知识点比较杂,不过都是面试中几乎常问的知识点,也是加分的点. 关于这部分内容,可能需要有一些具体的项目实践.在面试的过程中,结合具体自身 ...

  5. 7年老Android一次坑爹的面试经历,吊打面试官系列!

    前言 刚从阿里面试回来,想和大家分享一些我的面试经验,以及面试题目. 这篇文章将会更加聚焦在面试前需要看哪些资料,一些面试技巧以及一些这次的面试考题. 一个朋友是前阿里人,37岁,离职后就职美团.以前 ...

  6. 2021年最新Android大厂面试题来袭!吊打面试官系列!

    面试经验 自己大大小小投了也有20多家公司,不过经历简历筛选以及笔试淘汰,最终就经历了7家公司的面试.下面我就把自己面试中问到的问题贴出来供大家参考,一些具体项目相关的就不贴了. 阿里巴巴 阿里是3月 ...

  7. 线程停止继续_晓龙吊打面试官系列: 如何优雅的停止一个线程

    一.什么时候我们需要中断一个线程 在实际的开发中,有很多场景需要我们中断一个正在运行的线程,就比如: 当我们使用抢票软件时,其中某一个通道已经抢到了火车票,这个时候我们就需要通知其他线程停止工作. 当 ...

  8. 我去头条面试,面试官问我如何设计好API,看看我是如何吊打面试官的!

    作者 | 点击关注 ???? 来源 | Java开发宝典(ID:javakaifabaodian) 头图 | CSDN 下载自东方 IC "语言首先是写给人看的,只是恰巧(incidenta ...

  9. 2020面试题合集之吊打面试官系列(一),Android中为什么需要Handler

    合并式:addAssetPath时加入所有插件和主工程的路径:由于AssetManager中加入了所有插件和主工程的路径,因此生成的Resource可以同时访问插件和主工程的资源.但是由于主工程和各个 ...

最新文章

  1. 优雅地处理重复请求(并发请求)——附Java实现
  2. appium python框架结构,GitHub - wyybingo/python-appium: 基于PageObject UI自动化测试框架,支持Android/iOS...
  3. php dhcp,ip如何设置dhcp
  4. javascript对下拉列表框(select)的操作
  5. VB实训项目:学生成绩管理系统V1.0
  6. php脚本语法格式,[PHP学习笔记][五]PHP基本语法规则
  7. 抽象类继承 java_java继承抽象类
  8. kafka依赖_Kafka集群搭建及必知必会
  9. java 显示 装配_【spring】---spring的装配Bean方式
  10. 网络安全与信息安全【知识点】
  11. FishC笔记—29 讲 文件:一个任务
  12. Allegro Pad Designer焊盘制作指南
  13. 猿人时代java_猿人时代攻略
  14. 嵌入式软件未来发展趋势
  15. 人工智能实战2019第七次作业(黄金点) 16721088 焦宇恒
  16. 【软件测试】接口测试用例和报告模板
  17. 水粉中的这两种绘画方法?你经常用哪种?
  18. debian linux 硬盘,Debian硬盘安装
  19. 玩转开发板--Linux系统移植至开发板fl2440实践过程
  20. 解决 js 长任务导致的页面卡顿(时间分片技术)

热门文章

  1. 想跳槽?先看什么是好工作
  2. 学术英语视听说2听力原文_大学英语新世纪视听说第二册听力原文及答案免费...
  3. 零代码平台在大型企业的进化之路
  4. [bzoj5314][Jsoi2018]潜入行动_树形背包dp
  5. 【Python错误】Simplify chained comparison
  6. mysql 存储 emoji_MySQL 中如何存储 emoji ?
  7. BDD100K数据集简单解析以及格式转换成voc格式
  8. C/C++编程学习 - 第5周 ⑧ 判断直角三角形
  9. 测试四年工作心得:如何追求卓越
  10. AD-SAL与MD-SAL的比较