android 中的 Hybrid 开发

6,554 阅读46分钟

转载请标明出处:一片枫叶的专栏

上一篇文章中我们介绍了android开发中经常会涉及到但又常常被忽视掉的开发者模式。主要讲解了包括如何打开手机的开发者模式,开发者模式中各个菜单的意义和作用,如何清除手机App数据,以及清除手机App数据具体清除那些数据等知识点,具体关于android中开发者模式的知识,可参考我的: android产品研发(十六)–>开发者选项

本文将介绍android中hybrid开发相关的知识点。hybrid开发实际上是混合开发的意思,这里的混合是H5开发与Native开发混合的意思。下面的文章中我们将逐个介绍一下hybrid开发的概念、hybrid开发的优势、android中如何实现hybrid开发、简单的hybrid开发的例子,以及在产品实践中对hybrid开发的应用,希望通过本篇文章的介绍让您能够对android中的hybrid开发有一个基本的认识。

一:hybrid开发的概念

在具体介绍hybrid开发之前,我们先看一下什么是hybrid开发,在这里我们先引用一下百度百科中对hybrid开发的定义:

Hybrid App(混合模式移动应用)是指介于web-app、native-app这两者之间的app,兼具“Native App良好用户交互体验的优势”和“Web App跨平台开发的优势”。

从定义中我们可以看到hybrid开发其实就是在App开发过程中既使用到了web开发技术也使用到了native开发技术,通过这两种技术混合实现的App就是我们通常说的hybrid app,而通过这两种技术混合开发就是hybrid开发。

好吧,我们已经知道hybrid开发的具体含义,那么一个问题就产生了,既然我们已经有了native开发了为何还需要hybrid开发呢?它有什么好处么?答案是肯定的,下面我们就来看一下为何需要hybrid开发方式。

二:为何需要hybrid开发

下面我们简单看一下Native开发中存在的弊端以及使用hybrid开发方式的好处,通过对比你就能知道了hybrid开发的优势,当然了,这里不是推崇使用hybrid开发方式,native也有native开发的优势,hybrid开发也有hybrid开发的劣势,这里只是简单的看一下hybrid相对于native开发的优势。

  • 使用Native开发的方式人员要求高,只是一个简单的功能就需要IOS程序员和Android程序员各自完成;

  • 使用Native开发的方式版本迭代周期慢,每次完成版本升级之后都需要上传到App Store并审核,升级,重新安装等,升级成本高;

  • 使用hybrid开发的方式简单方便,同一套代码既可以在IOS平台使用,也可以在android平台使用,提高了开发效率与代码的可维护性;

  • 使用hybrid开发的方式升级简单方便,只需要服务器端升级一下就好了,对用户而言完全是透明了,免去了Native升级中的种种不便;

通过对比可以发现hybrid开发方式现对于native实现主要的优势就是更新版本快,代码维护方便,当然了这两个优点也是我们推崇使用hybrid开发app的主要因素。知道了hybrid开发的好处之后,我们如何在android中实现hybrid开发呢?下面我们就将介绍这个问题。

三:android中如何实现Bybird开发

其实在android开发中使用hybrid模式开发app,也是有两种方案的:

  • 使用第三方hybrid框架

  • 自己使用webview加载

通过这两种方案实现hybrid开发各有利弊,具体如下:

  • 使用PhoneGap、AppCan之类的第三方框架,其实现的原理是以WebView作为用户界面层,以Javascript作为基本逻辑,以及和中间件通讯,再由中间件访问底层API的方式,进行应用开发。相当于为我们封装了webview与相应的native组件;

  • 使用webview控件加载H5网页的内容,其中客户端的webview只是作为一个加载H5页面的壳子,具体的实现效果是由H5实现的,这个需要Native程序员和H5程序员一起合作完成;

  • 使用第三方框架的方式的好处是许多功能已经被集成好了,只需要简单的调用即可,但是这种方式集成度高,不容易定制化处理,而且性能上也是一个打的问题;

  • 使用webview加载H5页面,定制化程度高,问题可控,但是相对与第三方框架集成度不够高,但是其已经可以满足我们日常的开发功能需要了,目前还是比较推荐使用这种方式实现hybrid开发;

下面我们就看一下如何在android系统中通过webview实现对H5页面的加载操作。

四:hybrid开发简单实现

  • 在AndroidManifest.xml中定义网络请求权限

注意这个权限是必须的,因为加载webview页面一般而言经常是网络上的H5页面,这时候的网络请求权限就是必须的了,好多时候测试webview加载网络H5页面失败,找了半天不知道是什么原因,最后才发现是网络权限没有添加…

这里的WebView控件就是android原生的webview控件了,其和普通的android控件的使用没有什么不同都是在布局文件中定义,然后在Activity代码中获取并执行初始化操作等等。

  • 在代码中获取Webview控件加载本地或者网络H5资源

加载本地H5页面

/**
 * 加载本地H5资源文件
 */
webView = (WebView) findViewById(R.id.webView);
webView.loadUrl("file:///android_asset/example.html");

加载网络H5页面

/**
 * 加载网络H5资源
 */
webView = (WebView) findViewById(R.id.webView);
webView.loadUrl("http://baidu.com");

可以发现在获取到webview组件之后直接执行一个loadUrl方法传入一个url地址就可以了,这样在activity页面中就可以展示出webview页面了,契合普通的网页效果没什么不同,这里需要说明的是,webview不但能够加载网页地址,同样的也可以加载html代码,本地html资源等等,相对来说功能还是很强大的。

当然了以上只是最最简单的webview使用的例子,下面我们可以为我们的webview对象设置各种参数:

WebSettings webSettings = h5Fragment.mWebView.getSettings()
        webSettings.setJavaScriptEnabled(true)
        webSettings.setLoadWithOverviewMode(true)
        webSettings.setAllowFileAccess(false)
        webSettings.setUseWideViewPort(false)
        webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE)
        webSettings.setDatabaseEnabled(false)
        webSettings.setAppCacheEnabled(false)
        webSettings.setBlockNetworkImage(true)

这里的WebSettings就是webview的设置参数对象,我们是通过它为webview设置各种参数值的,见名知意,看见名字我们就知道各个set方法的意思了。比如设置webview中的html页面js代码是否可用,是否可以访问系统文件,H5缓存是否可用,是否立即加载网页图片等等。

  • 为Webview控件设置WebChromeClient

WebChromeClient对象是webview的关于页面效果回调方法的实现对象,主要用于实现webview页面上一些效果的回调,我们可以看一下其中实现的一些回调方法:

/**
 * 自定义实现WebChromeClient对象
 */
public class MWebChromeClient extends WebChromeClient{

    /**
     * 当webview加载进度变化时回调该方法
     */
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        super.onProgressChanged(view, newProgress);
    }

    /**
     * 当加载到H5页面title的时候回调该方法
     */
    @Override
    public void onReceivedTitle(WebView view, String title) {
        super.onReceivedTitle(view, title);
    }

    /**
     * 当接收到icon的时候回调该方法
     */
    @Override
    public void onReceivedIcon(WebView view, Bitmap icon) {
        super.onReceivedIcon(view, icon);
    }

    /**
     * 当H5页面调用js的Alert方法的时候回调该方法
     */
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        return super.onJsAlert(view, url, message, result);
    }

    /**
     * 当H5页面调用js的Confirm方法的时候回调该方法
     */
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
        return super.onJsConfirm(view, url, message, result);
    }

    /**
     * 当H5页面调用js的Prompt方法的时候回调该方法
     */
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        return super.onJsPrompt(view, url, message, defaultValue, result);
    }
}

上面的WebChromeClient中我们重写了其中的几个字方法,我们已经在方法中添加了注释标明了各个方法的调用时机,而且通过方法名我们也不难发现各个方法的具体作用,这里就不在具体的介绍了。

  • 为Webview主要设置WebviewClient
/**
 * 自定义实现WebViewClient类
 */
public class MWebViewClient extends WebViewClient {

    /**
     * 在webview加载URL的时候可以截获这个动作, 这里主要说它的返回值的问题:
     *  1、返回: return true;  webview处理url是根据程序来执行的。 
     *  2、返回: return false; webview处理url是在webview内部执行。 
     */
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {

    }

    /**
     * 在webview开始加载页面的时候回调该方法
     */
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);

    }

    /**
     * 在webview加载页面结束的时候回调该方法
     */
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
    }

    /**
     * 加载页面失败的时候回调该方法
     */
    
    @TargetApi(21)
    @Override
    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {

    }

    /**
     * 加载页面失败的时候回调该方法
     */
    /**
     * 在android23中改方法被onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) 替代
     * 因此在android23中执行替代方法
     * 在android23之前执行该方法
     * @param view
     * @param errorCode
     * @param description
     * @param failingUrl
     */
    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {

    }
}

这里我们只是暂时看一下WebViewClient中的几个比较重要的方法,shouldOverrideUrlLoading方法,onPageStarted方法,onPageFinished方法,onReceivedError方法等,相关的方法说明已经有注释了,这里就不在做过多的说明了。好了介绍完了相关的API之后我们来看一下我们在产品中关于hybrid开发的实践。

hybrid这么高逼格的东西友友用车怎么能不涉及呢?在我们的产品开发中也使用到了Webview,并封装了自己的Webview库,下面我们就看一下友友用车中关于hybrid开发的实践。

(1)定义H5Activity类,用于展示H5页面

/**
 * 自定义实现的H5Activity类,主要用于在页面中展示H5页面,整个Activity只有一个Fragment控件
 */
public class H5Activity extends BaseActivity {

    public H5Fragment h5Fragment = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_h5);
        h5Fragment = new H5Fragment();
        getSupportFragmentManager().beginTransaction().replace(R.id.mfl_content_container, h5Fragment).commit();
    }
}

(2)在H5Fragment中具体实现对H5页面的加载操作

/**
 * 具体实现H5页面加载Fragment,只有一个Webview控件
 */
public class H5Fragment extends BaseFragment implements SwipeRefreshLayout.OnRefreshListener {

    @BindView(R.id.sswipeRefreshLayout)
    public SwipeRefreshLayout swipeRefreshLayout;
    /**
     * H5页面 WebView
     */
    @BindView(R.id.mwebview)
    public WebView mWebView = null;
    @BindView(R.id.rl)
    public RelativeLayout rl;
    /**
     * 页面title
     */
    public String title = "";
    /**
     * 页面当前URL
     */
    public String currentUrl = "";
    /**
     * 判断网页是否加载成功
     */
    public boolean isSuccess = true;
    /**
     * 判断前一页H5是否需要刷新
     */
    public boolean isNeedFlushPreH5 = false;

    private BasePayFragmentUtils payFragmentUtils;

    public static final String KEY_DIALOG_WEB_VIEW = "dialog_webView";
    /**
     * 是否是弹窗中的WebView
     */
    private boolean isDialogWebView = false;

    View.OnClickListener errorOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mProgressLayout.showLoading();
            isSuccess = true;
            reflush();
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        payFragmentUtils = new BasePayFragmentUtils(this, BasePayFragmentUtils.ORDER_TYPE_H5);

        Bundle bundle = getArguments();
        if (bundle != null && bundle.containsKey(KEY_DIALOG_WEB_VIEW)) {
            isDialogWebView = bundle.getBoolean(KEY_DIALOG_WEB_VIEW, false);
        }
    }

    @Override
    public View setView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_h5, null);
        ButterKnife.bind(this, rootView);

        if (getActivity() instanceof H5Activity) {
            H5Activity h5Activity = (H5Activity) getActivity();
            mProgressLayout = h5Activity.mProgressLayout;
        }

        initView();
        initData();
        return rootView;
    }

    @Override
    public void onResume() {
        super.onResume();
        payFragmentUtils.onPayResume();
        if (H5Constant.isNeedFlush == true || isNeedFlushPreH5 == true) {
            H5Constant.isNeedFlush = false;
            isNeedFlushPreH5 = false;
            
            initData();
        }
    }

    /**
     * 执行组件初始化的操作
     */
    private void initView() {
        
        isSwipeEnable();
        
        H5FragmentUtils.initH5View(this);
        
        mWebView.setWebViewClient(new MWebViewClient(this));
        
        mWebView.setWebChromeClient(new WebChromeClient());

        if (isDialogWebView) {
            mProgressLayout.setCornerResId(R.drawable.map_confirm_bg);
        }
    }

    /**
     * 执行初始化加载数据的操作
     */
    private void initData() {
        mProgressLayout.showLoading();
        
        H5FragmentUtils.setTitle(this, title);
        
        currentUrl = H5FragmentUtils.getUrl(this, currentUrl);
        
        reflush();
    }

    /**
     * 判断下拉刷新组件是否可用
     */
    private void isSwipeEnable() {
        if (getActivity() == null) {
            return;
        }

        if (isDialogWebView) {
            getActivity().getIntent().putExtra(H5Constant.CARFLUSH, false);
        }
        
        if (getActivity().getIntent().getBooleanExtra(H5Constant.CARFLUSH, true)) {
            swipeRefreshLayout.setEnabled(true);
            swipeRefreshLayout.setColorSchemeResources(R.color.c1, R.color.c1, R.color.c1);
            swipeRefreshLayout.setOnRefreshListener(this);
            mWebView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_DOWN) {
                        int downY = (int) event.getY();
                        if (downY <= displayutil.screenhightpx="" 3)="" {="" swiperefreshlayout.setenabled(true);="" }="" else="" swiperefreshlayout.setenabled(false);="" return="" false;="" });="" **="" *="" 执行webview的下拉刷新操作="" @override="" public="" void="" onrefresh()="" reflush();="" 刷新当前页面="" private="" reflush()="" if="" (config.isnetworkconnected(mcontext))="" (!textutils.isempty(currenturl))="" h5cookie.syncookies(mcontext,="" currenturl,="" h5cookie.gettoken());="" mwebview.loadurl(currenturl);="" swiperefreshlayout.setrefreshing(false);="" mprogresslayout.showerror(erroronclicklistener);="" ondestroyview()="" super.ondestroyview();="" }<="" code="">

(3)初始化WebView组件

/**
     * 初始化组件WebView
     *
     * @param h5Fragment
     */
    public static void initH5View(H5Fragment h5Fragment) {
        if (h5Fragment == null || h5Fragment.getActivity() == null) {
            return;
        }

        if (h5Fragment.getActivity().getIntent().getBooleanExtra(H5Constant.SOFT_INPUT_IS_CHANGE_LAYOUT, false)) {
            h5Fragment.getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
        }
        
        
        WebSettings webSettings = h5Fragment.mWebView.getSettings();
        webSettings.setJavaScriptEnabled(true);
        webSettings.setLoadWithOverviewMode(true);
        webSettings.setAllowFileAccess(false);
        webSettings.setUseWideViewPort(false);
        webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
        webSettings.setDatabaseEnabled(false);
        webSettings.setAppCacheEnabled(false);
        webSettings.setBlockNetworkImage(true);
    }

(4)自定义实现WebviewClient对象

/**
 * 自定义实现WebviewClient类
 */
public class MWebViewClient extends WebViewClient {

    public H5Fragment h5Fragment = null;
    public Activity h5Activity = null;

    public MWebViewClient(H5Fragment h5Fragment) {
        this.h5Fragment = h5Fragment;
        if (h5Fragment.getActivity() == null) {
            h5Activity = Config.currentContext;
        } else {
            h5Activity = h5Fragment.getActivity();
        }
    }

    /**
     * 拦截H5页面的a标签跳转,解析scheme协议
     * 相当于放弃了a标签的使用,转而使用自定义的scheme协议
     */
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        
        if (url.indexOf(H5Constant.SCHEME) != -1) {
            try {
                Uri uri = Uri.parse(url);
                String[] urlSplit = url.split("\\?");
                Map queryMap = new HashMap();
                String h5Url = null;
                if (urlSplit.length == 2) {
                    queryMap = H5Constant.parseUriQuery(urlSplit[1]);
                    h5Url = queryMap.get(H5Constant.MURL);
                }
                
                {
                    
                    if (queryMap.containsKey(H5Constant.RELOADPRE) && "1".equals(queryMap.get(H5Constant.RELOADPRE))) {
                        h5Fragment.isNeedFlushPreH5 = true;
                    }
                    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                    h5Activity.startActivityForResult(intent, H5Constant.h5RequestCode);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return true;
        }
        
        else if (url.indexOf("tel://") != -1) {
            final String number = url.substring("tel://".length());
            Config.callPhoneByNumber(h5Activity, number);
            return true;
        } else if (url.indexOf("tel:") != -1) {
            final String number = url.substring("tel:".length());
            Config.callPhoneByNumber(h5Activity, number);
            return true;
        }
        
        else {
            view.loadUrl(url);
            
            return false;
        }
    }

    /**
     * H5页面刚刚开始被webview加载时回调该方法
     */
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);

    }

    /**
     * H5页面结束被加载时回调该方法
     */
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        h5Fragment.swipeRefreshLayout.setRefreshing(false);
        if (h5Activity.getTitle().toString().equals("找不到网页")) {
            h5Fragment.mProgressLayout.showError(h5Fragment.errorOnClickListener);
            return;
        }
        if (h5Fragment.isSuccess)
            h5Fragment.mProgressLayout.showContent();
        else
            h5Fragment.mProgressLayout.showError(h5Fragment.errorOnClickListener);

        h5Fragment.onLoadFinish(h5Fragment.isSuccess);
        if (h5Fragment.isSuccess) {
            h5Fragment.mWebView.getSettings().setBlockNetworkImage(false);
        }
    }

    
    @TargetApi(21)
    @Override
    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
        if (Build.VERSION.SDK_INT >= 21) {
            if (request.isForMainFrame()) {
                h5Fragment.isSuccess = false;
                h5Fragment.mProgressLayout.showError(h5Fragment.errorOnClickListener);
            }
        }
    }

    /**
     * 在android23中改方法被onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) 替代
     * 因此在android23中执行替代方法
     * 在android23之前执行该方法
     * @param view
     * @param errorCode
     * @param description
     * @param failingUrl
     */
    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        if (Build.VERSION.SDK_INT < 23) {
            h5Fragment.isSuccess = false;
            h5Fragment.mProgressLayout.showError(h5Fragment.errorOnClickListener);
        }
    }
}

(5)打开H5页面

Intent intent = new Intent(context, H5Activity.class)
        intent.putExtra(H5Constant.MURL, currentUrl)
        intent.putExtra(H5Constant.TITLE, title)
        context.startActivity(intent)

可以发现在产品实际开发过程中使用webview的页面都是整个的Activity页面,也就是说整个Activity页面只有一个webview控件,所以这时候页面的内容都是通过H5实现的。
然后当我们需要打开H5页面的时候可以通过服务器下发H5页面的url和title,并作为参数传递给H5Activity,然后打开该url所表示的网页。

同时我们使用Fragment用于实现加载H5页面的所以,所以以后当我们需要在其他地方使用加载H5页面的时候可以很方便的一直。

在MWebviewClient的shouldOverrideUrlLoading方法中我们拦截了所有的a标签跳转,转而实现我们自身的scheme协议,即a标签的跳转链接不再是常规的http链接,而是我们自定义的scheme协议,具体可参考:android产品研发(十一)–>应用内跳转scheme协议

总结:

  • 本文中我们介绍了hybrid开发的概念,hybrid开发的作用,android中如何实现hybrid开发,android实现hybrid的例子,产品中对hybrid开发的实践

  • 在定义webview的时候可以设置WebviewSettings,设置WebviewClient,设置WebChromeClient等参数对象

  • 可以在WebviewClient的shouldOverrideUrlLoading方法中拦截a标签的跳转并执行相应的逻辑


另外对产品研发技术,技巧,实践方面感兴趣的同学可以参考我的:
android产品研发(一)–>实用开发规范
android产品研发(二)–>启动页优化
android产品研发(三)–>基类Activity
android产品研发(四)–>减小Apk大小
android产品研发(五)–>多渠道打包
android产品研发(六)–>Apk混淆
android产品研发(七)–>Apk热修复
android产品研发(八)–>App数据统计
android产品研发(九)–>App网络传输协议
android产品研发(十)–>不使用静态变量保存数据
android产品研发(十一)–>应用内跳转scheme协议
android产品研发(十二)–>App长连接实现
android产品研发(十三)–>App轮训操作
android产品研发(十四)–>App升级与更新
android产品研发(十五)–>内存对象序列化
android产品研发(十六)–>开发者选项

本文以同步至github中:github.com/yipianfengy…,欢迎star和follow


转载请标明出处:一片枫叶的专栏

上一篇文章中我们讲解了android中内存对象的序列化方式。由于android开发涉及到不同Activity的数据传递,对于基本数据类型数据的传递是没有问题的,但是一旦涉及到复杂数据类型,就需要将数据序列化以便传输,在文章中我们主要讲解了两种数据序列化的方式:实现Serializable接口和实现Parcelable接口,同时也比较了它们各自的优缺点和实现方式。具体关于内存对象序列化方面的知识可参考:android产品研发(十五)–>内存对象序列化

本文主要介绍Android开发中常常涉及到但又不是被人重视知识点:开发者选项。主要涉及到如何打开开发者模式,开发者选项中有哪些操作菜单以及各自的作用,如何清除手机数据,清除手机数据具体清除那些数据等等。

一般而言,不同的手机开发者选项界面是不太相同的,这是由于手机的设置界面都被做了定制化处理,但是其基本的功能菜单都是类似的。下面我们就先来看一下如何打开手机的开发者模式。

如何打开开发者选项菜单?

不同的手机进入开发者选项的菜单可能不太一样,但是基本的大概的可能是:

  • 关于手机

  • android版本号

  • 连续点击N次

  • 弹出进入开发者模式说明

经过上面的步骤,我们就打开了手机的开发者模式,在进入了开发者模式之后我们就可以在设置页面或者是设置里面的其他设置,高级设置等等菜单之中找找是否出现了开发者选项的菜单,若出现了开发者选项菜单我们就可以根据自己的需求选择性的打开各种控制开关了。

开发者选项中提供了那些功能?

知道了如何把手机进入开发者模式之后,在我们的日常开发过程中,不可避免的会使用到android开发者选项这一个功能,比如使用真机在android studio中调试App等等,那么开发者选项中到底有哪些功能呢?一下就是开发者选项中提供的功能呢列表:

这里写图片描述

开发者选项中的具体功能

这里以红米note2的开发者选项说明一下各个选项的具体功能:

  • 开启开发者选项
    这是开发者选项的控制开发,打开这个才算开启了开发者选项,并且下面的选项功能才可以使用

  • 提交错误报告
    将本机上安卓系统的出错日志以及硬件设备信息发送给谷歌。一般是发送不到的,原因你懂的!所以开不开启都无所谓的。

  • 不锁定屏幕
    解释很清楚,充电时不会休眠,比如我们在使用手机调试程序的时候,一会手机就锁屏了,很麻烦,如果我们打开这个设置之后,无论什么时候我们的手机都不会在锁屏了,很方便

  • 直接进入系统
    很实用,就是开发过程中点击屏幕直接进入系统而不会锁屏

  • 打开蓝牙数据包日志
    这个选项会抓取所有的蓝牙数据包保存到一个文件中,在调试蓝牙程序的时候比较好用

  • 进程统计信息
    主要用于统计系统程序的后台信息
    这里写图片描述
    可以查看一些程序使用时长,内存占用等信息;

  • USB调试
    这是手机能够连接电脑的关键操作,只有开启了这个选项手机才能连接到电脑,并进行调试,很多时候我们的手机连接不到电脑都是因为我们打开了开发者模式,但是允许USB调试的开关没有打开,这时候重新打开USB调试,可能手机就能连接到电脑了

  • 允许模拟位置
    允许代码模拟位置,比如地图类应用需要测试在外地的使用情况,通过开启此项选项可以通过代码模拟位置

  • 选择调试应用
    设定需要调试的应用程序,以android studio为例,设定调试程序之后,Android monitor窗口的默认选择程序就是设定的调试程序。当然我们也可以在手机的开发者选项中选择需要调试的应用程序

  • 显示触摸操作
    可以在屏幕中显性的展示触摸的轨迹

  • 指针位置
    可以显示触摸的指针坐标点

  • 显示边界布局
    主要用于显示布局的边界,比如一个Activity显示界面中各种布局文件的边界等

  • 窗口动画缩放
    可以设置动画的缩放效果

  • 动画程序时常缩放
    可以设置动画程序播放时长

  • 模拟辅助显示设备
    小米手机中改选项可以模拟各种屏幕分辨路的显示效果

  • 调试GPU过度绘制
    主要用于显示在界面是否存在过度绘制的现象
    一共有四种颜色:蓝色、绿色、淡红、深红。根据过度绘制的次数,依次递增。1x过度绘制是蓝色、2x是绿色、3x是淡红、4x是深红。具体关于android中过度绘制的问题,可参考我的:android中的过度绘制

  • 后台进程限制
    主要用于限制后台进程的数量

  • 系统内存优化级别
    主要用于设置系统内存的优化级别

当然了以上介绍的这些选项是开发者选项中提供核心功能的菜单,此外还有一些其他选项,大家可以多了解一下。

清除App数据

下面我们将在开发者选项的基础上介绍一个其他方面的内容–清除App数据。

什么是清除App数据?

手机在运行过程中会在手机端保存一些临时数据,配置数据,运行数据等,这些数据可能以配置文件,数据库文件等形式保存在手机端,android手机在设置页面提供了清除App数据的功能,可以通过这个功能实现对App保存数据的清除操作。

如何进行清除App数据

我们可以通过如下步骤实现对App数据的清除工作:

这样通过如上的操作步骤我们就将这个App的数据清除了,但是这样操作之后到底会清除App那些数据呢?

清除App数据的类型

  • 这里新建一个项目com.chao.ttext,我们在项目数据目录:data/data/com.chao.ttext目录下创建缓存数据目录,具体目录结果如下所示:
data/data/com.chao.ttext # ls
lib 存放使用的包
files 存放应用程序自己保存的文件
databases 存放数据库数据
shared_prefs SP文件
cache 存放缓存数据
app_appcache H5缓存
app_databases webview缓存
app_geolocation 定位缓存
  • 然后我们为每个目录添加一个新的空文件,这里暂时使用linux命令:touch,在每个目录中添加数据文件用于判断清除数据的结果:
/data/data/com.chao.ttext # touch lib/temp.txt
/data/data/com.chao.ttext # touch files/temp.txt
/data/data/com.chao.ttext # touch databases/temp.txt
/data/data/com.chao.ttext # touch shared_prefs/temp.txt
/data/data/com.chao.ttext # touch cache/temp.txt
/data/data/com.chao.ttext # touch app_appcache/temp.txt
/data/data/com.chao.ttext # touch app_databases/temp.txt
/data/data/com.chao.ttext # touch app_geolocation/temp.txt
  • 继续的我们执行清除App数据的操作,即:打开设置-》应用管理-》ttext-》清除数据

  • 最后我们查看一下执行了清除数据操作之后的数据目录即查看ttext数据目录下的数据情况:

/data/data/com.chao.ttext # ls
lib

然后进入lib目录查看temp.txt文件是否还存在,结果还是存在的。

结论:清除数据会清除App数据目录下除lib文件以外的所有文件和目录。

总结:

  • 在android开发中常常会使用到开发者选项,可以通过设置关于手机android版本号连续点击的方式进入开发者选项

  • 常见的手机无法连接电脑可能是USB调试开关没有打开的原因,可以尝试打开USB调试连接电脑

  • 开发者选项中有一些比较实用的功能可能会在调试App的时候用到,比如:不锁屏,GPU调试,动画调试等等

  • 清除App数据会清除App数据目录下除lib文件以外的所有文件和目录

  • 清除App数据,会使App进程被杀死,也就是说执行了清除App数据的操作之后再次打开App都是重新打开一个新的进程


另外对产品研发技术,技巧,实践方面感兴趣的同学可以参考我的:
android产品研发(一)–>实用开发规范
android产品研发(二)–>启动页优化
android产品研发(三)–>基类Activity
android产品研发(四)–>减小Apk大小
android产品研发(五)–>多渠道打包
android产品研发(六)–>Apk混淆
android产品研发(七)–>Apk热修复
android产品研发(八)–>App数据统计
android产品研发(九)–>App网络传输协议
android产品研发(十)–>不使用静态变量保存数据
android产品研发(十一)–>应用内跳转scheme协议
android产品研发(十二)–>App长连接实现
android产品研发(十三)–>App轮训操作
android产品研发(十四)–>App升级与更新
android产品研发(十五)–>内存对象序列化

本文以同步至github中:github.com/yipianfengy…,欢迎star和follow


转载请标明出处:一片枫叶的专栏

上一篇文章中我们讲解了android app中的升级更新操作,app的升级更新操作算是App的标配了,升级操作就是获取App的升级信息,更新操作是下载,安装,更新app,其中我们既可以使用app store获取应用的升级信息,也可以在应用内通过请求本地服务器获取应用的升级信息,并通过与本地app的版本号对比判断应用是否需要升级。
升级信息是app更新的基础,只有我们的app的升级信息指明需要更新,我们才可以开始后续的更新操作–也就是下载安装更新app。这里强调一点的是应用的升级操作分为普通的升级和强制升级两种,普通的升级操作就是完成一次对app的更新操作,而强制更新是在线上的app出现bug 的时候一种强制用户升级的手段,用户体验不太好,所以一般不太建议使用这种方式升级用户的app。
更多关于app升级更新的信息,可参考我的:android产品研发(十四)–>App升级与更新

本文将讲解android中数据传输中需要了解的数据序列化方面的知识,我们知道android开发过程中不同Activity之间传输数据可以通过Intent对象的put**方法传递,对于java的八大基本数据类型(char int float double long short boolean byte)传递是没有问题的,但是如果传递比较复杂的对象类型(比如对象,比如集合等),那么就可能存在问题,而这时候也就引入了数据序列化的概念。

序列化的定义:

这里我们先看一下呢序列化在百科上的定义

序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

简单来说就是我们的数据在传输的时候需要将信息转化为可以传输的数据,然后在传输的目标方能够反序列化将数据还原回来,这里的将对象状态信息转换为可传输数据的过程就是序列化,将可传输的数据逆还原为对象的过程就是反序列化。

为什么需要序列化:

知道前面的序列化定义,内存对象什么需要实现序列化呢?

  • 永久性保存对象,保存对象的字节序列到本地文件。

  • 通过序列化对象在网络中传递对象。

  • 通过序列化对象在进程间传递对象。

实现序列化的两种方式:

那么我们如何实现序列化的操作呢?在android开发中我们实现序列化有两种方式:

  • 实现Serializable接口

  • 实现parcelable接口

两种序列化方式的区别:

都知道在android studio中序列化有两种方式:serializable与parcelable。那么这两种实现序列化的方式有什么区别呢?下面是这两种实现序列化方式的区别:

  1. Serializeble是java的序列化方式,Parcelable是android特有的序列化方式;

  2. 在使用内存的时候,Parcelable比Serializable性能高,所以推荐使用Parcelable。

  3. Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。

  4. Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性在外界有变化的情况下。尽管Serializable效率低点, 也不提倡用,但在这种情况下,还是建议你用Serializable。

最后还有一点就是Serializeble序列化的方式比较简单,直接集成一个接口就好了,而parcelable方式比较复杂,不仅需要集成Parcelable接口还需要重写里面的方法。

对象实现序列化的实例:

通过实现Serializable接口实现序列化:

上面介绍了那么多概念上的知识,下面我们就具体看一下如何通过这两种方式实现序列化,我们首先看一下如何通过实现Serializable接口实现序列化,通过实现Serializable接口实现序列化,只需要简单的实现Serialiizable接口即可。通过实现Serializable接口就相当于标记类型为序列化了,不需要做其他的操作了。

/**
 * Created by aaron on 16/6/29.
 */
public class Person implements Serializable{

    private int age;
    private String name;
    private String address;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

可以发现我们定义了一个普通的实体Person类,并设置了三个成员属性以及各自的set,get方法,然后我们就只是简单的实现了Serializable接口就相当于将该类序列化了,当我们在程序中传输该类型的对象的时候就没有问题了。

通过实现Parcelable接口实现序列化:

然后我们在看一下通过实现Parcelable接口来实现序列化的方式,通过实现Parcelable接口实现序列化相当于实现Serialiable接口稍微复杂一些,因为其需要实现一些特定的方法,下面我们还是以我们定义的Person类为例子,看一下如果是实现Parcelable接口具体是如何实现的:

/**
 * Created by aaron on 16/6/29.
 */
public class Person implements Parcelable{

    private int age;
    private String name;
    private String address;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.age);
        dest.writeString(this.name);
        dest.writeString(this.address);
    }

    public Person() {
    }

    protected Person(Parcel in) {
        this.age = in.readInt();
        this.name = in.readString();
        this.address = in.readString();
    }

    public static final Creator CREATOR = new Creator() {
        @Override
        public Person createFromParcel(Parcel source) {
            return new Person(source);
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };
}

可以发现当我们通过实现Parcelable接口实现序列化还需要重写里面的成员方法,并且这些成员方法的写法都比较固定。

实现Parcelable序列化的android studio插件:

顺便说一下最近发现一个比较不错的Parcelable序列化插件。下面就来看一下如何安装使用该插件。

  • 打开android studio –> settings –> Plugins –> 搜索Parcelable –> Install –> Restart,这样安装好了Parcelable插件;

这里写图片描述

  • 然后在源文件中右键 –> Generate… –> Parcelable

这里写图片描述

  • 点击Parcelable之后可以看到,源文件中已经实现了Parcelable接口,并重写了相应的方法:

这里写图片描述

这样我们就安装好Parcelable插件了,然后当我们执行Parcelable操作的时候就重写了Parcelable接口的相应序列化方法了。

总结:

  • 可以通过实现Serializable和Parcelable接口的方式实现序列化

  • 实现Serializable接口是java中实现序列化的方式,而实现Parcelable是android中特有的实现序列化的方式,更适合android环境

  • 实现Serializable接口只需要实现该接口即可无需其他操作,而实现Parcelable接口需要重写相应的方法

  • android studio中有实现Parcelable接口的相应插件,可安装该插件很方便的实现Parcelable接口,实现序列化


另外对产品研发技术,技巧,实践方面感兴趣的同学可以参考我的:
android产品研发(一)–>实用开发规范
android产品研发(二)–>启动页优化
android产品研发(三)–>基类Activity
android产品研发(四)–>减小Apk大小
android产品研发(五)–>多渠道打包
android产品研发(六)–>Apk混淆
android产品研发(七)–>Apk热修复
android产品研发(八)–>App数据统计
android产品研发(九)–>App网络传输协议
android产品研发(十)–>不使用静态变量保存数据
android产品研发(十一)–>应用内跳转scheme协议
android产品研发(十二)–>App长连接实现
android产品研发(十三)–>App轮训操作
android产品研发(十四)–>App升级与更新

本文以同步至github中:github.com/yipianfengy…,欢迎star和follow


转载请标明出处:一片枫叶的专栏

上一篇文章中我们讲解了android app中的轮询操作,讲解的内容主要包括:我们在App中使用轮询操作的情景,作用以及实现方式等。一般而言我们使用轮询操作都是通过定时任务的形式请求服务器并更新用户界面,轮询操作都有一定的使用生命周期,即在一定的页面中启动轮询操作,然后在特定的情况下关闭轮询操作,这点需要我们尤为注意,我们还介绍了使用Timer和Handler实现轮询操作的实例,更多关于App中轮询操作的信息,可参考我的:android产品研发(十三)–>App轮询操作

本文将讲解app的升级与更新。一般而言用户使用App的时候升级提醒有两种方式获得:

  • 一种是通过App Store获取

  • 一种是打开应用之后提醒用户更新升级

而更新操作一般是在用户点击了升级按钮之后开始执行的,这里的升级操作也分为两种形式:

app升级操作:

在App Store中升级需要为App Store上传新版App,我们在新版本完成之后都会上传到App Store中,不同的应用市场审核的时间不同,一般除了第一次上传时间较长之外,其余的审核都是挺快的,一般不会超过半天(不排除例外情况奥),在审核完成之后就相当于完成了这个应用市场的发布了,也就是发布上线了。这时候如果用户安装了这个应用市场,那么就能看到我们的App有新版本的升级提醒了。

除了可以在应用市场升级,我们还可以在应用内升级,在应用内升级主要是通过调用服务器端接口获取应用的升级信息,然后通过获取的服务器升级应用信息与本地的App版本比对,若服务器下发的最新的App版本高于本地的版本号,则说明有新版本发布,那么我们就可以执行更新操作了,否则忽略掉即可。

应用内升级其实已经有好多第三方的SDK了,常见的友盟,百度App开发工具包都已经集成了升级的功能,部分SDK厂商还提供增量更新的功能。增量更新的内容不是我们这里的讨论重点,想了解更多增量更新的内容可参考:浅谈Android增量升级

这里我们先简单介绍一下友盟的App升级功能,友盟其实已经有了App升级的API,我们只需要简单的调用即可。

/**
 * 请求友盟更新API,判断是否弹出更新弹窗
 */
public static void updateVersion(final Activity mContext, final MainActivity.UpdateCallback updateCallback, final boolean isShow) {
        UmengUpdateAgent.setUpdateListener(new UmengUpdateListener() {
            @Override
            public void onUpdateReturned(int updateStatus, UpdateResponse updateInfo) {
                switch (updateStatus) {
                    
                    case UpdateStatus.Yes: 
                        try {
                            
                            String value = MobclickAgent.getConfigParams(mContext, "FORCE_UPDATE_MIXVERSION");
                            if (value != null && !value.trim().equals("")) {
                                int versionCode = Config.changeVersionNameToCode(value);
                                if (versionCode != 0) {
                                    String localVersionName = getVersionName(mContext);
                                    int localVersionCode = Config.changeVersionNameToCode(localVersionName);
                                    
                                    if (localVersionCode <= versioncode)="" {="" updatecallback.onupdatesuccess(updateinfo);="" }="" else="" umengupdateagent.setupdateautopopup(true);="" umengupdateagent.showupdatedialog(mcontext,="" updateinfo);="" catch="" (exception="" e)="" e.printstacktrace();="" break;="" case="" updatestatus.no:="" if="" (isshow)="" config.showtoast(mcontext,="" "您当前使用的友友用车已是最新版本");="" });="" umengupdateagent.setupdateautopopup(false);="" umengupdateagent.forceupdate(mcontext);="" umengupdateagent.setchannel(channelutil.getchannel(mcontext));="" }<="" code="">

以上是友盟的升级API,在调用之前需要先继承友盟的SDK,这样经过调用之后我们就可以通过友盟实现更新接口的提示功能了,默认的友盟提供了静默安装,更新提示弹窗,强制更新等几种,可以根据自身App的需求来确定更新的方式。

如果不喜欢使用第三方的更新方式,我们也可以通过调用服务器接口的方式实现自己的更新弹窗提示,主要的逻辑也是通过判断服务器下发的最新App版本号与本地版本号对比,若服务器端的App版本号大于本地的App版本号,则说明当前App不是最新的版本,需要升级,这里我们简单看一下友友用车中自定义的更新接口实现:

/**
     * 检测App是否需要更新
     *
     * @param mContext
     * @param isShow   若不需要更新是否需要弹出文案
     */
    public static void queryAppBaseVersionInfo(final Activity mContext, final boolean isOneUpdate, final boolean isShow) {
        try {
            
            if (!Config.isNetworkConnected(mContext)) {
                
                dismissProgress(isShow);
                return;
            }
            
            if (isQueryAppUpdated && isOneUpdate) {
                return;
            }
            L.i("开始调用请求是否需要版本更新的接口....");
            ExtInterface.QueryAppBaseVersionInfoNL.Request.Builder request = ExtInterface.QueryAppBaseVersionInfoNL.Request.newBuilder();
            request.setClientChannel(CHANNEL_ANDROID);
            
            
            NetworkTask task = new NetworkTask(Cmd.CmdCode.QueryAppBaseVersionInfo_VALUE);
            task.setBusiData(request.build().toByteArray());
            NetworkUtils.executeNetwork(task, new HttpResponse.NetWorkResponse() {
                @Override
                public void onSuccessResponse(UUResponseData responseData) {
                    if (responseData.getRet() == 0) {
                        try {
                            isQueryAppUpdated = true;
                            ExtInterface.QueryAppBaseVersionInfoNL.Response response = ExtInterface.QueryAppBaseVersionInfoNL.Response.parseFrom(responseData.getBusiData());
                            if (response.getRet() == 0) {
                                L.i("请求检测App是否更新接口成功,开始解析返回结果");
                                
                                parserUpdateResule(mContext, response, isShow);
                            } else {
                                if (isShow) {
                                    showDefaultNetworkSnackBar(mContext);
                                }
                            }
                        } catch (InvalidProtocolBufferException e) {
                            e.printStackTrace();
                            if (isShow) {
                                showDefaultNetworkSnackBar(mContext);
                            }
                        }
                    }
                }

                @Override
                public void onError(VolleyError errorResponse) {
                    L.e("请求检测更新接口失败....");
                    if (isShow) {
                        showDefaultNetworkSnackBar(mContext);
                    }
                }

                @Override
                public void networkFinish() {
                    L.i("请求检测更新接口完成....");
                    
                    dismissProgress(isShow);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

该接口只会在App打开时调用一次,判断App是否需要更新,然后在请求服务器成功之后,会解析请求结果,我们继续看一下我们的解析逻辑:

/**
     * 解析更新检查结果
     *
     * @param response
     */
    private static void parserUpdateResule(Activity mContext, ExtInterface.QueryAppBaseVersionInfoNL.Response response, boolean isShow) {
        if (mContext == null) {
            return;
        }
        
        ExtInterface.AppBaseVersionInfo appBaseVersionInfo = response.getAppBaseVersionInfo();
        
        if (appBaseVersionInfo.getIsDel() == ENEFFECT) {
            return;
        }
        String updateVersionCode = appBaseVersionInfo.getVersionCode();
        int updateCode = changeVersionNameToCode(updateVersionCode);
        int localCode = changeVersionNameToCode(VersionUtils.getVersionName(mContext));
        
        L.i("本地版本号:" + localCode + "  " + VersionUtils.getVersionName(mContext) + "  远程版本号:" + updateCode
                            + "  " + updateVersionCode);
        if (localCode < updateCode) {
            
            L.i("开始显示更新弹窗...");
            showUpdateDialog(mContext, appBaseVersionInfo);
        }
        
        else {
            if (isShow) {
                Config.showToast(mContext, mContext.getResources().getString(R.string.about_new));
            }
        }
    }

解析更新接口信息的时候,会判断App的更新操作是普通更新还是强制更新,若是强制更新的话,则没有取消按钮,并且更新弹窗不可关闭。若是普通的更新的话则有暂不更新按钮,点击暂不更新更新弹窗会取消,但是当下次打开App的时候,弹窗提醒还是会弹窗。

普通更新包含暂不更新和立即更新两个按钮操作:
这里写图片描述

强制更新只有立即更新按钮,弹窗不可取消:
这里写图片描述

app更新操作:

app的更新操作就是下载App并安装了,下面我们还是分两部分看,应用市场的更新与应用内更新

在应用市场中更新App很简单就是执行简单的下载操作,然后顺着App的提醒,一步步安装即可,这里没有什么需要注意的地方。

应用内更新操作主要是当用户点击了更新按钮之后执行的,下载,安装等逻辑,下面我们看一下友友用车应用内更新的实践。

应用内更新主要包含了:普通更新和强制更新两种,其中普通更新弹窗可以选择更新也可以选择忽略,而强制更新只能选择更新,并且更新弹窗不可取消。

下面的代码是执行下载操作的核心逻辑:

/**
     * 开始执行下载动作
     */
    private static void doDownLoad(final Activity mContext, String downloadUrl, final String actionButtonMsg, final boolean isFocusUpdate) {
        
        if (isFocusUpdate) {
            DownLoadDialog.updateRela.setVisibility(View.VISIBLE);
            DownLoadDialog.progressBar.setProgress(0);
            DownLoadDialog.progressBar.start();
            DownLoadDialog.updatePercent.setText("0%");
            DownLoadDialog.materialDialog.getPositiveButton().setEnabled(false);
            DownLoadDialog.materialDialog.getPositiveButton().setText("下载中");
        }
        Config.showToast(mContext, "开始下载安装包.......");
        
        doDeleteDownApk(mContext);
        L.i("安装包下载地址:" + downloadUrl);
        DownloadManager.getInstance().cancelAll();
        DownloadManager.downloadId = DownloadManager.getInstance().add(DownloadManager.getDownLoadRequest(mContext, downloadUrl, new DownloadStatusListenerV1() {
            @Override
            public void onDownloadComplete(DownloadRequest downloadRequest) {
                L.i("onDownloadComplete_____...");
                
                showPositiveText(false, actionButtonMsg);
                if (isFocusUpdate) {
                    
                    DownLoadDialog.updatePercent.setText("100%");
                    DownLoadDialog.progressBar.stop();
                } else {
                    String title = "正在下载友友用车...";
                    String content = "下载成功";
                    DownloadNotification.showNotification(mContext, title, content, DownloadNotification.notofyId);
                    
                    UUApp.notificationManager.cancel(DownloadNotification.notofyId);
                }
                
                doInstallApk(mContext);
                
                UUApp.getInstance().exit();
            }

            @Override
            public void onDownloadFailed(DownloadRequest downloadRequest, int errorCode, String errorMessage) {
                L.i("onDownloadFiled______...");
                L.i("errorMessage:" + errorMessage);
                
                showPositiveText(false, actionButtonMsg);
                if (isFocusUpdate) {
                    
                    DownLoadDialog.updatePercent.setText("更新失败");
                } else {
                    String title = "正在下载友友用车...";
                    String content = "下载失败";
                    DownloadNotification.showNotification(mContext, title, content, DownloadNotification.notofyId);
                }
            }

            @Override
            public void onProgress(DownloadRequest downloadRequest, long totalBytes, long downloadedBytes, int progress) {
                if (lastProgress != progress) {
                    lastProgress = progress;
                    L.i("onProgress_____progress:" + progress + "  totalBytes:" + totalBytes + "  downloadedBytes:" + downloadedBytes);
                    
                    showPositiveText(true, actionButtonMsg);
                    
                    if (isFocusUpdate) {
                        String content = downloadedBytes * 100 / totalBytes + "%";
                        float result = progress / (float)100.00;
                        DownLoadDialog.progressBar.setProgress(result);
                        DownLoadDialog.updatePercent.setText(content);
                    } else {
                        String title = "正在下载友友用车...";
                        String content = downloadedBytes * 100 / totalBytes + "%";
                        DownloadNotification.showNotification(mContext, title, content, DownloadNotification.notofyId);
                    }
                }
            }
        }));
    }

这里的下载操作包含了三个回调方法:

  • onDownloadComplete()

  • onDownloadFailed()

  • onProgress()

其中onDownlaodComplete方法在下载完成时回调,onDownloadFailed方法在下载失败是回调,而onProgress方法则用于刷新下载进程,我们在onProcess方法中更新通知栏下载进度,具体我们可以看一下更新通知栏消息的方法:

/**
     * 更新通知栏显示
     * @param title
     * @param content
     * @param notifyId
     */
    public static void showNotification(Activity mContext, String title, String content, int notifyId) {
        NotificationCompat.Builder mNotifyBuilder = new NotificationCompat.Builder(mContext)
                .setSmallIcon(R.mipmap.icon)
                .setContentTitle(title)
                .setContentText(content)
                .setSmallIcon(android.R.drawable.stat_sys_download);

        Notification notification = mNotifyBuilder.build();
        
        UUApp.notificationManager.notify(notifyId, notification);
    }

而在onDownloadFailed方法中,执行的代码逻辑是提示用户下载失败,
而在onDownloadComplete方法中,执行安装下载apk文件的操作,我们可以继续看一下我们是如何执行安装逻辑的。

/**
     * 执行安装apk文件
     */
    private static void doInstallApk(Activity mContext) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(Uri.fromFile(new File(DownloadManager.getApkPath(mContext))),
                "application/vnd.android.package-archive");
        mContext.startActivity(intent);
    }

这段代码会调用android的安装apk程序,这样我们就执行了下载文件的安装操作,不同的手机安装程序及展示界面略有不同。

总结:

  • App升级操作分为两种,在应用市场提示升级和在应用内提示升级,而在应用内提示升级可以继承第三方升级API(如:友盟),也可以自己实现;

  • 应用升级的提示主要逻辑是根据服务器端的APK版本号与本地的应用版本号对比,若服务器端的应用版本号高于本地版本号,则说明应用需要升级;

  • 应用升级可以分为普通升级和强制升级两种,一般不太建议使用强制升级(用户体验很差),除非是一些严重的线上bug;

  • App的更新操作包含下载与安装两部分,下载操作时可以选择继承第三方服务,也可以自己实现。


另外对产品研发技术,技巧,实践方面感兴趣的同学可以参考我的:
android产品研发(一)–>实用开发规范
android产品研发(二)–>启动页优化
android产品研发(三)–>基类Activity
android产品研发(四)–>减小Apk大小
android产品研发(五)–>多渠道打包
android产品研发(六)–>Apk混淆
android产品研发(七)–>Apk热修复
android产品研发(八)–>App数据统计
android产品研发(九)–>App网络传输协议
android产品研发(十)–>不使用静态变量保存数据
android产品研发(十一)–>应用内跳转scheme协议
android产品研发(十二)–>App长连接实现
android产品研发(十三)–>App轮询操作

本文以同步至github中:github.com/yipianfengy…,欢迎star和follow


转载请标明出处:一片枫叶的专栏

上一篇文章中我们讲解了android app实现长连接的几种方式,各自的优缺点以及具体的实现,一般而言使用第三方的推送服务已经可以满足了基本的业务需求,当然了若是对技术有追求的可以通过NIO或者是MINA实现自身的长连接服务,但是自己实现的长连接服务一来比较复杂耗时比较多,而且可能过程中有许多坑要填,一般而言推荐使用第三方的推送服务,稳定简单,具体管理长连接部分的模块可参考:android产品研发(十二)–>App长连接实现

而本文将讲解app端的轮询请求服务,一般而言我们经常将轮询操作用于请求服务器。比如某一个页面我们有定时任务需要时时的从服务器获取更新信息并显示,比如当长连接断掉之后我们可能需要启动轮询请求作为长连接的补充等,所以这时候就用到了轮询服务。

什么是轮询请求

在说明我们轮询请求之前,这里先说明一下什么叫轮询请求,我的理解就是App端每隔一定的时间重复请求的操作就叫做轮询请求,比如:App端每隔一段时间上报一次定位信息,App端每隔一段时间拉去一次用户状态等,这些应该都是轮询请求,那么前一篇我们讲了App端的长连接,为什么我们有了长连接之后还需要轮询操作呢?

这是因为我们的长连接并不是稳定的可靠的,而我们执行轮询操作的时候一般都是要稳定的网络请求,而且轮询操作一般都是有生命周期的,即在一定的生命周期内执行轮询操作,而我们的长连接一般都是整个进程生命周期的,所以从这方面讲也不太适合。

轮询请求实践

与长连接相关的轮询请求

  1. 上一篇我们在讲解长连接的时候说过长连接有可能会断,而这时候在长连接断的时候我们就需要启动一个轮询服务,它作为长连接的补充。
/**
     * 启动轮询服务
     */
    public void startLoopService() {
        
        
        if (!LoopService.isServiceRuning) {
            
            if (UserConfig.isPassLogined()) {
                
                if (MinaLongConnectManager.session != null && MinaLongConnectManager.session.isConnected()) {
                    LoopService.quitLoopService(context);
                    return;
                }
                LoopService.startLoopService(context);
            } else {
                LoopService.quitLoopService(context);
            }
        }
    }

这里就是我们执行轮询服务的操作代码,其作用就是启动了一个轮询service(即轮询服务),然后在轮询服务中执行具体的轮询请求,既然这样我们就具体看一下这个service的代码逻辑。

/**
 * 长连接异常时启动服务,长连接恢复时关闭服务
 */
public class LoopService extends Service {

    public static final String ACTION = "com.youyou.uuelectric.renter.Service.LoopService";

    /**
     * 客户端执行轮询的时间间隔,该值由StartQueryInterface接口返回,默认设置为30s
     */
    public static int LOOP_INTERVAL_SECS = 30;
    /**
     * 轮询时间间隔(MLOOP_INTERVAL_SECS 这个时间间隔变量有服务器下发,此时轮询服务的场景与逻辑与定义时发生变化,涉及到IOS端,因此采用自己定义的常量在客户端写死时间间隔)
     */
    public static int MLOOP_INTERVAL_SECS = 30;
    /**
     * 当前服务是否正在执行
     */
    public static boolean isServiceRuning = false;
    /**
     * 定时任务工具类
     */
    public static Timer timer = new Timer();

    private static Context context;

    public LoopService() {
        isServiceRuning = false;
    }

    

    /**
     * 启动轮询服务
     */
    public static void startLoopService(Context context) {
        if (context == null)
            return;
        quitLoopService(context);
        L.i("开启轮询服务,轮询间隔:" + MLOOP_INTERVAL_SECS + "s");
        AlarmManager manager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
        Intent intent = new Intent(context.getApplicationContext(), LoopService.class);
        intent.setAction(LoopService.ACTION);
        PendingIntent pendingIntent = PendingIntent.getService(context.getApplicationContext(), 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        
        /**
         * 闹钟的第一次执行时间,以毫秒为单位,可以自定义时间,不过一般使用当前时间。需要注意的是,本属性与第一个属性(type)密切相关,
         * 如果第一个参数对应的闹钟使用的是相对时间(ELAPSED_REALTIME和ELAPSED_REALTIME_WAKEUP),那么本属性就得使用相对时间(相对于系统启动时间来说),
         *      比如当前时间就表示为:SystemClock.elapsedRealtime();
         * 如果第一个参数对应的闹钟使用的是绝对时间(RTC、RTC_WAKEUP、POWER_OFF_WAKEUP),那么本属性就得使用绝对时间,
         *      比如当前时间就表示为:System.currentTimeMillis()。
         */
        manager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), MLOOP_INTERVAL_SECS * 1000, pendingIntent);
    }

    /**
     * 停止轮询服务
     */
    public static void quitLoopService(Context context) {
        if (context == null)
            return;
        L.i("关闭轮询闹钟服务...");
        AlarmManager manager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
        Intent intent = new Intent(context.getApplicationContext(), LoopService.class);
        intent.setAction(LoopService.ACTION);
        PendingIntent pendingIntent = PendingIntent.getService(context.getApplicationContext(), 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        manager.cancel(pendingIntent);
        
        L.i("关闭轮询服务...");
        context.stopService(intent);
    }

    @Override
    public void onCreate() {
        super.onCreate();

        context = getApplicationContext();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        L.i("开始执行轮询服务... \n 判断当前用户是否已登录...");
        
        if (UserConfig.isPassLogined()) {
            
            L.i("当前用户已登录... \n 判断长连接是否已经连接...");
            if (MinaLongConnectManager.session != null && MinaLongConnectManager.session.isConnected()) {
                L.i("长连接已恢复连接,退出轮询服务...");
                quitLoopService(context);
            } else {
                if (isServiceRuning) {
                    return START_STICKY;
                }
                
                startLoop();
            }
        } else {
            L.i("用户已退出登录,关闭轮询服务...");
            quitLoopService(context);
        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        L.i("轮询服务退出,执行onDestory()方法,inServiceRuning赋值false");
        isServiceRuning = false;
        timer.cancel();
        timer = new Timer();
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    /**
     * 启动轮询拉去消息
     */
    private void startLoop() {
        if (timer == null) {
            timer = new Timer();
        }
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                isServiceRuning = true;
                L.i("长连接未恢复连接,执行轮询操作... \n 轮询服务中请求getInstance接口...");
                LoopRequest.getInstance(context).sendLoopRequest();
            }
        }, 0, MLOOP_INTERVAL_SECS * 1000);
    }
}

可以发现这里的service轮询服务的代码量还是比较多的,但是轮询服务请求代码注释已经很详细了,所以就不做过多的说明,需要说明的是其核心就是通过Timer对象每个一段时间执行一次网络请求。具体的网络请求代码:

L.i("长连接未恢复连接,执行轮询操作... \n 轮询服务中请求getInstance接口...")

LoopRequest.getInstance(context).sendLoopRequest()

这里的轮询服务请求核心逻辑:当长连接出现异常时,启动轮询服务,并通过Timer对象每隔一定时间拉取服务器状态,当长连接恢复时,关闭轮询服务。这就是我们与长连接有关的轮询服务的代码执行逻辑,看完这部分之后我们再看一下与页面相关的轮询请求的执行逻辑。

与页面相关的轮询请求

  • 与页面相关的轮询请求
    我们的App中当用户停留在某一个页面的时候我们可能需要定时的拉取用户状态,这时候也需要使用轮询请求拉取服务器状态,当用户离开该页面的时候关闭轮询服务请求。

这里我们看一下我们产品当前行程页面的轮询操作,用于轮询请求当前用户的车辆里程,费用,用时等信息,具体可参考下图:

其实在当前Fragment页面有一个定时的拉去订单信息的轮询请求,下面我们具体看一下这个定时请求的执行逻辑:

/**
 * TimerTask对象,主要用于定时拉去服务器信息
 */
public class Task extends TimerTask {
        @Override
        public void run() {
            L.i("开始执行执行timer定时任务...");
            handler.post(new Runnable() {
                @Override
                public void run() {
                    isFirstGetData = false;
                    getData(true);
                }
            });
        }
    }

而这里的getData方法就是拉去服务器状态的方法,这里不做过多的解释,当用户退出这个页面的时候需要清除这里的轮询操作。所以在Fragment的onDesctoryView方法中执行了清除timerTask的操作。

@Override
    public void onDestroyView() {
        super.onDestroyView();
        ...
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
        if (timerTask != null) {
            timerTask.cancel();
            timerTask = null;
        }
        ...
    }

这样当用户打开这个页面的时候初始化TimerTask对象,每个一分钟请求一次服务器拉取订单信息并更新UI,当用户离开页面的时候清除TimerTask对象,即取消轮询请求操作。可以发现上面我们看到的与长连接和页面相关的轮询请求服务都是通过timer对象的定时任务实现的轮询请求服务,下面我们看一下如何通过Handler对象实现轮询请求服务。

通过Handler对象实现轮询请求

  • 下面我们来看一个通过Handler异步消息实现的轮询请求服务。
/**
     * 默认的时间间隔:1分钟
     */
    private static int DEFAULT_INTERVAL = 60 * 1000;
    /**
     * 异常情况下的轮询时间间隔:5秒
     */
    private static int ERROR_INTERVAL = 5 * 1000;
    /**
     * 当前轮询执行的时间间隔
     */
    private static int interval = DEFAULT_INTERVAL;
    /**
     * 轮询Handler的消息类型
     */
    private static int LOOP_WHAT = 10;
    /**
     * 是否是第一次拉取数据
     */
    private boolean isFirstRequest = false;
    /**
     * 第一次请求数据是否成功
     */
    private boolean isFirstRequestSuccess = false;

    /**
     * 开始执行轮询,正常情况下,每隔1分钟轮询拉取一次最新数据
     * 在onStart时开启轮询
     */
    private void startLoop() {
        L.i("页面onStart,需要开启轮询");
        loopRequestHandler.sendEmptyMessageDelayed(LOOP_WHAT, interval);
    }

    /**
     * 关闭轮询,在界面onStop时,停止轮询操作
     */
    private void stopLoop() {
        L.i("页面已onStop,需要停止轮询");
        loopRequestHandler.removeMessages(LOOP_WHAT);
    }

    /**
     * 处理轮询的Handler
     */
    private Handler loopRequestHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {

            
            if (!isFirstRequestSuccess) {
                L.i("首次请求失败,需要将轮询时间设置为:" + ERROR_INTERVAL);
                interval = ERROR_INTERVAL;
            } else {
                interval = DEFAULT_INTERVAL;
            }

            L.i("轮询中-----当前轮询间隔:" + interval);

            loopRequestHandler.removeMessages(LOOP_WHAT);

            
            if (!isFirstRequestSuccess || !Config.locationIsSuccess()) {
                isClickLocationButton = false;
                doLocationOption();
            } else {
                loadData();
            }

            System.gc();

            loopRequestHandler.sendEmptyMessageDelayed(LOOP_WHAT, interval);

        }
    };

这里是通过Handler实现的轮询操作,其核心原理就是在handler的handlerMessage方法中,接收到消息之后再次发送延迟消息,这里的延迟时间就是我们定义的轮询间隔时间,这样当我们下次接收到消息的时候又一次发送延迟消息,从而造成我们时时发送轮询消息的情景。

以上就是我们实现轮询操作的两种方式:

  • Timer对象实现轮询操作

  • Handler对象实现轮询操作

上面我们分析了轮询请求的不同使用场景,作用以及实现方式,当我们在具体的开发过程中需要定时的向服务器拉取消息的时候就可以考虑使用轮询请求了。

总结:

  • 轮询操作一般都是通过定时请求服务器拉取信息并更新UI;

  • 轮询操作一般都有一定的生命周期,比如在某个页面打开时启动轮询操作,在某个页面关闭时取消轮询操作;

  • 轮询操作的请求间隔需要根据具体的需求确定,间隔时间不宜过短,否则可能造成并发性问题;

  • 产品开发过程中,某些需要试试更新服务器拉取信息并更新UI时,可以考虑使用轮询操作实现;

  • 可以通过Timer对象和Handler对象两种方式实现轮询请求操作;


另外对产品研发技术,技巧,实践方面感兴趣的同学可以参考我的:
android产品研发(一)–>实用开发规范
android产品研发(二)–>启动页优化
android产品研发(三)–>基类Activity
android产品研发(四)–>减小Apk大小
android产品研发(五)–>多渠道打包
android产品研发(六)–>Apk混淆
android产品研发(七)–>Apk热修复
android产品研发(八)–>App数据统计
android产品研发(九)–>App网络传输协议
android产品研发(十)–>不使用静态变量保存数据
android产品研发(十一)–>应用内跳转scheme协议
android产品研发(十二)–>App长连接实现

本文以同步至github中:github.com/yipianfengy…,欢迎star和follow