22

这篇文章最近登上了 HackerNews 的榜首:http: //highscalability.com/blog/2013/9/18/if-youre-programming-a-cell-phone-like-a-server-youre-doing.html#

其中指出:

手机收音机是手机上最大的电池消耗之一。每次发送数据,无论多小,收音机都会开机20-30秒。您做出的每一个决定都应基于尽量减少无线电启动的次数。通过更改应用程序处理数据传输的方式,可以显着延长电池寿命。用户现在想要他们的数据,诀窍是平衡用户体验与传输数据并最大限度地减少电力使用。通过应用程序将所有重复和间歇性传输小心地捆绑在一起,然后积极预取间歇性传输来实现平衡。

我想修改以添加一个选项,例如“现在$.ajax不需要完成,只需在启动另一个请求时执行此请求”。什么是解决这个问题的好方法?

我从这个开始:

(function($) {
    var batches = [];
    var oldAjax = $.fn.ajax;
    var lastAjax = 0;
    var interval = 5*60*1000; // Should be between 2-5 minutes
    $.fn.extend({batchedAjax: function() {
        batches.push(arguments);
    }});
    var runBatches = function() {
        var now = new Date().getTime();
        var batched;
        if (lastAjax + interval < now) {
            while (batched = batches.pop()) {
                oldAjax.apply(null, batched);
            }
        }
    }
    setInterval(runBatches, interval);
    $.fn.ajax = function() {
        runBatches();
        oldAjax.apply(null, arguments);
        lastAjax = now;
    };
})(jQuery);

我无法通过论文的措辞来判断,我猜一个好的批处理“间隔”是 2-5 分钟,所以我只使用了 5。

这是一个很好的实现吗?

  • ajax通过向方法添加{batchable:true}选项,我怎样才能使它成为对方法的真正修改?我也不是很清楚。

  • 是否setInterval也让手机一直处于唤醒状态?这是一件坏事吗?有没有更好的方法不这样做?

  • 这里还有其他东西会导致电池更快耗尽吗?

  • 这种方法是否值得?现代智能手机同时发生了很多事情,如果我的应用程序没有使用手机,那么肯定有其他应用程序在使用。Javascript 无法检测到单元格是否打开,那何必呢?值得打扰吗?

4

6 回答 6

7

我在添加选项方面取得了一些进展$.ajax,开始编辑问题,并意识到作为答案更好:

(function($) {
    var batches = [];
    var oldAjax = $.fn.ajax;
    var lastAjax = 0;
    var interval = 5*60*1000; // Should be between 2-5 minutes
    var runBatches = function() {
        var now = new Date().getTime();
        var batched;
        if (lastAjax + interval < now) {
            while (batched = batches.pop()) {
                oldAjax.apply(null, batched);
            }
        }
    }
    setInterval(runBatches, interval);
    $.fn.ajax = function(url, options) {
        if (options.batchable) {
            batches.push(arguments);
            return;
        }
        runBatches();
        oldAjax.apply(null, arguments);
        lastAjax = now;
    };
})(jQuery);

这实际上相当简单。很高兴看到更好的答案。

于 2013-09-20T05:26:44.333 回答
5
  • setInterval 是否也让手机一直保持清醒?这是一件坏事吗?有没有更好的方法不这样做?

从 iPhone 4、iOS 6.1.0 Safari 环境:

A 编写了一个带有倒数计时器的应用程序,该计时器以一秒的间隔更新元素的文本。DOM 树具有中等复杂性。该应用程序是一个相对简单的计算器,没有执行任何 AJAX。然而,我总是偷偷地怀疑那些每秒一次的回流会害死我。每当我把它放在桌子上打开时,我的电池肯定会很快耗尽,而 Safari 在应用程序的网页上。

该应用程序中只有两次超时。现在,我没有任何可量化的证据表明超时正在耗尽我的电池,但是这个笨拙的计算器每 45 分钟损失大约 10%有点令人不安。(谁知道呢,也许是背光。)

关于这一点:您可能想要构建一个测试应用程序,它在间隔上执行 AJAX,在间隔上执行其他操作等,并比较每个函数在类似条件下如何耗尽电池电量。获得受控环境可能很棘手,但如果排水量差异足够大,那么即使是“不完美”的测试条件也会产生足够明显的结果,让您得出结论。


然而,我发现了一个有趣的事情,关于 iOS 6.1.0 Safari 如何处理超时:

  • 如果您关闭屏幕,超时不会运行它们的回调。
  • 因此,长期超时将“错过它们的标记”。

如果我的应用程序的计时器要显示正确的时间(即使在我关闭并重新打开屏幕之后),那么我就不能走简单的路线并执行secondsLeft -= 1. 如果我关闭屏幕,那么secondsLeft(相对于我的开始时间)将会“落后”,因此不正确。(屏幕关闭时setTimeout 回调未运行。)

解决方案是我必须timeLeft = fortyMinutes - (new Date().getTime() - startTime)在每个间隔上重新计算。

此外,我的应用程序中的计时器应该从绿色变为石灰,变为黄色,变为红色,因为它接近到期。由于此时我担心间隔代码的效率,我怀疑最好将颜色更改“安排”到适当的时间(石灰:开始时间后 20 分钟,黄色:30 分钟,红色:35)(这似乎比在每个间隔上进行四重不等式检查更可取,这在 99% 的情况下都是徒劳的)。

但是,如果我安排了这样的颜色更改,并且我的手机屏幕在目标时间关闭,那么颜色更改将永远不会发生。

解决方案是在每个时间间隔检查自上次 1 秒计时器更新以来经过的时间是否为“>= 2秒”。(这样,应用程序可以知道我的手机是否关闭了屏幕;它能够意识到它何时“落后”。)那时,如果有必要,我会“强制”应用颜色更改和计划下一个。

(不用说,我后来去掉了换色器……)

所以,我相信这证实了我的主张

如果屏幕关闭,iOS 6.1.0 Safari 不会执行 setTimeout 回调函数。

因此,在“调度”您的 AJAX 调用时请记住这一点,因为您可能也会受到这种行为的影响。

而且,使用我的提议,我可以回答你的问题:

  • 至少对于 iOS,我们知道 setTimeout 在屏幕关闭时休眠。
  • 因此 setTimeout 不会给您的手机带来“噩梦”(“保持清醒”)。

  • 这种方法是否值得?现代智能手机同时发生了很多事情,如果我的应用程序没有使用手机,那么肯定有其他应用程序在使用。Javascript 无法检测到单元格是否打开,那何必呢?值得打扰吗?

如果你能让这个实现正常工作,那么它似乎值得的。

您发出的每个 AJAX 请求都会产生延迟,这会在一定程度上降低您的应用程序的速度。(毕竟延迟是页面加载时间的祸根。)所以你肯定会通过“捆绑”请求获得一些收益。扩展 $.ajax 以便您可以“批量”请求肯定会有一些优点。

于 2013-09-30T02:24:22.090 回答
3

您链接的文章显然侧重于优化应用程序的功耗(是的,天气小部件示例令人恐惧)。根据定义,主动使用浏览器是一项前台任务;加上像ApplicationCache这样的东西已经可以用来减少对网络请求的需求。然后,您可以根据需要以编程方式更新缓存并避免 DIY。

持怀疑态度的旁注:如果您使用 jQuery 作为 HTML5 应用程序的一部分(可能包含在 Sencha 或类似中),那么移动应用程序框架可能更多地与请求优化有关,而不是代码本身。我没有任何证据,但该死的,这听起来是对的:)

  • ajax通过向方法添加{batchable:true}选项,我怎样才能使它成为对方法的真正修改?我也不是很清楚。

一种完全有效的方法,但对我来说,这听起来像是打鸭子出了问题。我不会。即使您正确默认batchable为 false,我个人更愿意使用外观(甚至可能在其自己的名称空间中?)

var gQuery = {}; //gQuery = green jQuery, patent pending :)
gQuery.ajax = function(options,callback){
  //your own .ajax with blackjack and hooking timeouts, ultimately just calling
  $.ajax(options);
}
  • 是否setInterval也让手机一直处于唤醒状态?这是一件坏事吗?有没有更好的方法不这样做?

和的本机实现setIntervalsetTimeoutafaik 非常相似;当网站在后台显示网上银行不活动提示时,考虑后者不会触发;当页面不在前台时,它的执行基本上会停止。如果 API 可用于此类“延迟”(文章提到了一些相关的 iOS7 功能),那么它可能是一种更可取的方法,否则我认为没有理由避免setInterval.

  • 这里还有其他东西会导致电池更快耗尽吗?

我推测任何繁重的负载都会(从计算pi到漂亮的 3d 转换)。但这对我来说听起来像是过早的优化,让我想起了具有省电模式的电子阅读器,它完全关闭了 LCD 屏幕:)

  • 这种方法是否值得?现代智能手机同时发生了很多事情,如果我的应用程序没有使用手机,那么肯定有其他应用程序在使用。Javascript 无法检测到单元格是否打开,那何必呢?值得打扰吗?

这篇文章指出一个天气应用程序过于贪婪,这让我很担心。这似乎是一个开发疏忽,尽管比其他任何事情都重要,因为获取数据的频率超过了实际需要的频率。在理想的世界中,这应该在操作系统级别上得到很好的处理,否则您最终会遇到一系列相互竞争的解决方法。IMO:在高可扩展性发布另一篇文章告诉您之前不要打扰:)

于 2013-09-30T00:48:49.057 回答
1

这是我的版本:

(function($) {
    var batches = [],
        ajax = $.fn.ajax,
        interval =  5*60*1000, // Should be between 2-5 minutes
        timeout = setTimeout($.fn.ajax, interval);

    $.fn.ajax=function(url, options) {
        var batched, returns;
        if(typeof url === "string") {
            batches.push(arguments);
            if(options.batchable) {
                return;
            }
        }
        while (batched = batches.shift()) {
            returns = ajax.apply(null, batched);
        }
        clearTimeout(timeout);
        timeout = setTimeout($.fn.ajax, interval);
        return returns;
    }
})(jQuery);

我认为这个版本有以下主要优点:

  • 如果存在不可批处理的 ajax 调用,则使用该连接发送所有批处理。这将重置计时器。
  • 返回直接 ajax 调用的预期返回值
  • 可以通过不带参数调用 $.fn.ajax() 来触发批次的直接处理
于 2013-09-26T19:40:46.640 回答
1

至于破解$.ajax方法,我会:

  • 尽量保留由Promise提供的机制$.ajax
  • 利用全局 ajax 事件之一来触发 ajax 调用,
  • $.ajax也许添加一个计时器,以便在没有“立即”调用的情况下调用批处理,
  • 给这个函数起一个新名字(在我的代码$.batchAjax中:)并保留原来的$.ajax.

这是我的出发点:

(function ($) {
    var queue = [],
        timerID = 0;

    function ajaxQueue(url, settings) {
    // cutom deferred used to forward the $.ajax' promise
        var dfd = new $.Deferred();

        // when called, this function executes the $.ajax call
        function call() {
            $.ajax(url, settings)
            .done(function () {
                dfd.resolveWith(this, arguments);
            })
            .fail(function () {
                dfd.rejectWith(this, arguments);
            });
        }

        // set a global timer, which will trigger the dequeuing in case no ajax call is ever made ...
        if (timerID === 0) {
            timerID = window.setTimeout(ajaxCallOne, 5000);
        }

        // enqueue this function, for later use
        queue.push(call);
        // return the promise
        return dfd.promise();
    }

    function ajaxCallOne() {
        window.clearTimeout(timerID);
        timerID = 0;

        if (queue.length > 0) {
            f = queue.pop();
        // async call : wait for the current ajax events
        //to be processed before triggering a new one ...
            setTimeout(f, 0);
        }
    }

    // use the two functions :

    $(document).bind('ajaxSend', ajaxCallOne);
    // or : 
    //$(document).bind('ajaxComplete', ajaxCallOne);

    $.batchAjax = ajaxQueue;
}(jQuery));

在此示例中,5 秒的硬编码延迟违背了“如果两次通话之间的时间少于 20 秒,它会耗尽电池”的目的。你可以放一个更大的(5 分钟?),或者完全删除它——这完全取决于你的应用程序。

小提琴


关于一般问题“我如何编写一个不会在 5 分钟内烧毁手机电池的网络应用程序?” : 对付那一支需要不止一支魔法箭。这是您必须做出的一整套设计决策,这实际上取决于您的应用程序。

您将不得不在一次加载尽可能多的数据(并可能发送不会使用的数据)与获取您需要的数据(并可能发送许多小的单独请求)之间进行仲裁。

要考虑的一些参数是:

  • 数据量(您也不想耗尽客户的数据计划......),
  • 服务器负载,
  • 可以缓存多少,
  • “最新”的重要性(聊天应用程序延迟 5 分钟是行不通的),
  • 客户端更新频率(网络游戏可能需要客户端进行大量更新,而新闻应用程序可能更少......)。

一个相当普遍的建议:您可以添加一个“实时更新”复选框,并存储其状态客户端。未选中时,客户端应点击“刷新”按钮以下载新数据。

于 2013-09-27T13:12:20.753 回答
0

这是我的出发点,它有点源于@Joe Frambach 发布的内容,但我想要以下补充:

  1. 如果提供了 jXHR 和错误/成功回调,则保留它们
  2. 消除相同的请求(通过 url 和选项匹配),同时仍然触发为每个调用提供的回调或 jqXHR
  3. 使用 AjaxSettings 使配置更容易
  4. 不要让每个非批处理 ajax 刷新批处理,这些应该是 IMO 单独的进程,但因此也提供了强制批处理刷新的选项。

无论哪种方式,这个傻瓜很可能最好作为一个单独的插件完成,而不是覆盖和影响默认的 .ajax 函数......享受:

(function($) {
    $.ajaxSetup({
        batchInterval: 5*60*1000,
        flushBatch: false,
        batchable: false,
        batchDebounce: true
    });

    var batchRun = 0;
    var batches = {};
    var oldAjax = $.fn.ajax;

    var queueBatch = function(url, options) {
        var match = false;
        var dfd = new $.Deferred();
        batches[url] = batches[url] || [];
        if(options.batchDebounce || $.ajaxSettings.batchDebounce) {

            if(!options.success && !options.error) {
                $.each(batches[url], function(index, batchedAjax) {
                    if($.param(batchedAjax.options) == $.param(options)) {
                        match = index;
                        return false;
                    }
                });
            }

            if(match === false) {
                batches[url].push({options:options, dfds:[dfd]});
            } else {
                batches[url][match].dfds.push(dfd);
            }

        } else {
            batches[url].push({options:options, dfds:[dfd]);
        }
        return dfd.promise();
    }

    var runBatches = function() {
        $.each(batches, function(url, batchedOptions) {
            $.each(batchedOptions, function(index, batchedAjax) {
                oldAjax.apply(null, url, batchedAjax.options).then(
                    function(data, textStatus, jqXHR) {
                        var args = arguments;
                        $.each(batchedAjax.dfds, function(index, dfd) {
                            dfd.resolve(args);
                        });
                    }, function(jqXHR, textStatus, errorThrown) {
                        var args = arguments;
                        $.each(batchedAjax.dfds, function(index, dfd) {
                            dfd.reject(args);
                        });
                    }
                )
            });
        });
        batches = {};
        batchRun = new Date.getTime();
    }

    setInterval(runBatches, $.ajaxSettings.batchInterval);

    $.fn.ajax = function(url, options) {
        if (options.batchable) {
            var xhr = queueBatch(url, options);
            if((new Date.getTime()) - batchRun >= options.batchInterval) {
                runBatches();
            }
            return xhr;
        }
        if (options.flushBatch) {
            runBatches();
        }
        return oldAjax.call(null, url, options);
    };
})(jQuery);
于 2013-09-30T22:04:15.037 回答