之前一直用友盟的自动更新功能,但是友盟一直没有内置实现强制更新的功能,如果要在其基础上模拟实现会很麻烦,所以干脆就自己做了。
其实实现上比较简单,这里跟大家介绍下。
1. web接口
需要提供一个接口供客户端查询更新状态,并且在需要更新时,告知客户端新APK地址。
接口参数如下:
- package 包名,因为有时候会出现同一个应用换包名打包的情况
- version 版本号,即android清单文件里面的versionCode
- channel 渠道号
- os 操作系统,android/ios。ios 这里仅作预留。
之所以传入这些字段,是要在与服务器端的包匹配时,务必满足:
package, channel, os 相等,并且服务器端的version 大于 客户端传入的version
代码如下:
os = request.GET.get('os') pkg_name = request.GET.get('package') channel = request.GET.get('channel') version = request.GET.get('version') if not os or not pkg_name or not channel or not version: return jsonify(**ret_dict) pkg = Package.objects.filter( os=os, package=pkg_name, channel=channel, status__gt=config.PACKAGE_STATUS_NOT_UPDATE ).order_by('-version').first() if pkg and int(version) < pkg.version: ret_dict['pkg_status'] = str(pkg.status) ret_dict['pkg_url'] = config.WEB_HOST + pkg.file.url ret_dict['update_prompt'] = pkg.info return jsonify(**ret_dict)
2. 数据库设计
由于web端使用的是django,所以可以很方便的给出运营同学可以操作的后台界面,如下:
注意红框内的元素,运营同学在上传时,是不允许修改的,而是由程序自动解析APK文件得到后填入的。
具体的解析方法,我们稍后给出。
而对应的models代码如下:
class Package(models.Model): file = models.FileField(u'文件', upload_to=config.PACKAGE_UPLOAD_PATH) package = models.CharField(u'包名', max_length=255, blank=True, default='') version = models.IntegerField(u"版本号", blank=True, default=0, null=True) channel = models.CharField(u"渠道", max_length=128, blank=True, default='') status = models.IntegerField(u'更新状态', default=config.PACKAGE_STATUS_NOT_UPDATE, choices=config.PACKAGE_UPDATE_STATUS) info = models.TextField(u'通知信息', blank=True, null=True) os = models.CharField(u'操作系统', max_length=64, default=config.PACKAGE_CLIENT_UNKNOW, choices=config.PACKAGE_CLIENT_OS, blank=True, null=True) def __unicode__(self): _,name = os.path.split(self.file.name) return name class Meta: unique_together = ('package', 'version', 'channel', 'os') def save(self, * args, ** kwargs): # 文件上传成功后,文件名会加上PACKAGE_UPLOAD_PATH路径 path,_ = os.path.split(self.file.name) if not path: if self.file.name.endswith('.apk'): self.os = config.PACKAGE_CLIENT_ANDROID path = os.path.join('/tmp', uuid.uuid4().hex + self.file.name) # logger.error('path: %s', path) with open(path, 'wb+') as destination: for chunk in self.file.chunks(): destination.write(chunk) info = parse_apk_info(path) os.remove(path) self.package = info.get('package', '') self.version = info.get('version', 0) self.channel = info.get('channel', '') elif self.file.name.endswith('ipa'): self.os = config.PACKAGE_CLIENT_IOS super(self.__class__, self).save(*args, ** kwargs) def display_filename(self): _,name = os.path.split(self.file.name) return name display_filename.short_description = u"文件"
3. APK文件解析
def parse_apk_info(apk_path, tmp_dir='/tmp'): """ 获取包名、版本、渠道: {'version': '17', 'channel': 'CN_MAIN', 'package': ‘com.fff.xxx'} :param apk_path: :return: """ from bs4 import BeautifulSoup import os import shutil import uuid abs_apk_path = os.path.abspath(apk_path) dst_dir = os.path.join(tmp_dir, uuid.uuid4().hex) jar_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'apktool.jar')) cmd = 'java -jar %s d %s %s' % (jar_path, abs_apk_path, dst_dir) if isinstance(cmd, unicode): cmd = cmd.encode('utf8') # 执行 os.system(cmd) manifest_path = os.path.join(dst_dir, 'AndroidManifest.xml') result = dict() with open(manifest_path, 'r') as f: soup = BeautifulSoup(f.read()) result.update( version=soup.manifest.attrs.get('android:versioncode'), package=soup.manifest.attrs.get('package'), ) channel_soup = soup.find('meta-data', attrs={'android:name': 'UMENG_CHANNEL'}) if channel_soup: result['channel'] = channel_soup.attrs['android:value'] shutil.rmtree(dst_dir) return result
当然,正如大家所看到的,我们需要依赖于 apktool.jar 这个文件,具体大家可以在网上下载。
ok,整个就是这样。
在线工具 on #
这个不是android热更,只是做新版本提醒是吧?
Reply
Dante on #
对的,这个不是热更新,是整个替换APK
Reply
风满楼i on #
通过apktool反编译获得AndroidManifest.xml文件,然后提取出其中的包名和版本号。Python确实好用简洁!
Reply
Dante on #
是的,哈哈
Reply