Nico随笔


  • 首页

  • 归档

  • 标签

快速打包方案

发表于 2017-11-01 | 分类于 Android快速打包

往apk文件里面动态添加信息的两种方法。

(都需要保留安装时候的apk文件)

  1. 方案一:往meta_info里面添加文件,写入参数信息
  2. 方案二:在apk这个文件的comment信息里面添加需要传入的信息

方案一:

参考:http://tech.meituan.com/mt-apk-packaging.html
主要代表:美团
原理:meta_info(里面存的就是签名信息)里面的文件不参与签名,修改后,apk不需要签名

方案二:

参考:http://blog.csdn.net/kongpinde/article/details/51518466
主要代表:天猫、豌豆荚
原理:apk就是一个zip压缩包。而zip包有个comment区域,可以往里面写入信息,而不对apk的安装产生影响
zip 文件的末尾有一个 Central Directory Record 区域,其末尾包含一个 File comment 区域,可以存放一些数据,所以 File comment 是 zip 文件一部分,如果可以正确的修改这个部分,就可以在不破坏压缩包、不用重新打包的的前提下快速的给 Apk 文件写入自己想要的数据。

comment 是在 Central Directory Record 末尾储存的,可以将数据直接写在这里,下表是 header 末尾的结构。
image
从表中可以看到定义 comment 长度的字段位于 comment 之前。

这里我们需要自定义 comment,在自定义 comment 内容的后面添加一个区域储存 comment 的长度,结构如下图。
image

Server动态生成apk

这一部分可以在本地或服务端进行,需要定义一个长度为 2 的 byte[] 来储存 comment 的长度,直接使用 Java 的 api 就可以把 comment 和 comment 的长度写到 Apk 的末尾,代码如下:

public static void writeApk(File file, String comment) {
    ZipFile zipFile = null;
    ByteArrayOutputStream outputStream = null;
    RandomAccessFile accessFile = null;
    try {
        zipFile = new ZipFile(file);
        String zipComment = zipFile.getComment();
        // 判断comment区域是否已经有数据了
        if (zipComment != null)
            return;
        byte[] byteComment = comment.getBytes();
        outputStream = new ByteArrayOutputStream();
        // 将数据写入输出流
        outputStream.write(byteComment);
        // 紧接着写入数据大小
        outputStream.write(short2Stream((short) byteComment.length));
            byte[] data = outputStream.toByteArray();
        accessFile = new RandomAccessFile(file, "rw");
        // 跳到comment区域
        accessFile.seek(file.length() - 2);
        // 先写入数据大小
        accessFile.write(short2Stream((short) data.length));
        // 写入数据
        accessFile.write(data);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (zipFile != null)
                zipFile.close();
            if (outputStream != null)
                outputStream.close();
            if (accessFile != null)
                accessFile.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
private static byte[] short2Stream(short data) {
    ByteBuffer buffer = ByteBuffer.allocate(2);
    buffer.order(ByteOrder.LITTLE_ENDIAN);
    buffer.putShort(data);
    buffer.flip();
    return buffer.array();
}

客户端解析apk数据:

private static String readApk(Context context) {
    // 获取文件路径
    File file = new File(context.getPackageCodePath());
    byte[] bytes = null;
    RandomAccessFile accessFile = null;
    try {
        accessFile = new RandomAccessFile(file, "r");
        long index = accessFile.length();
        bytes = new byte[2];
        // 获取comment文件的位置
        index = index - bytes.length;
        accessFile.seek(index);
        // 获取comment中写入数据的大小byte类型
        accessFile.readFully(bytes);
        // 将byte转换成大小
        int contentLength = stream2Short(bytes, 0);
        // 创建byte[]数据大小来存储写入的数据
        bytes = new byte[contentLength];
        index = index - bytes.length;
        accessFile.seek(index);
        // 读取数据
        accessFile.readFully(bytes);
        return new String(bytes, "utf-8");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (accessFile != null) {
            try {
                accessFile.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return null;
}


private static short stream2Short(byte[] stream, int offset) {
    ByteBuffer buffer = ByteBuffer.allocate(2);
    buffer.order(ByteOrder.LITTLE_ENDIAN);
    buffer.put(stream[offset]);
    buffer.put(stream[offset + 1]);
    return buffer.getShort(0);
}

apk的安装过程

  1. 复制APK安装包到data/app目录下(所以安装完成后,即使把sd卡中的apk删除也没关系);
  2. 解压并扫描安装包,把dex文件(Dalvik字节码)保存到dalvik-cache目录;
  3. 并data/data目录下创建对应的应用数据目录。

应用安装涉及到如下几个目录:

system/app —————系统自带的应用程序,获得adb root权限才能删除

data/app —————用户程序安装的目录。安装时把 apk文件复制到此目录
data/data —————存放应用程序的数据
data/dalvik-cache——–将apk中的dex文件安装到dalvik-cache目录下(dex文件是dalvik虚拟机的可执行文件,其大小约为原始apk文件大小的四分之一)

app卸载

删除安装过程中在上述三个目录下创建的文件及目录。

App核心指标和广告计费方式

发表于 2017-10-20 | 分类于 Android

App核心指标

  1. pv
  2. uv
  3. ctr
  4. 次日/7日留存/30日留存
    一般目标是在40/20/10
    参考文章:http://www.pmcaff.com/discuss/index/369885221299264?pmc_param=1

核心在分析哪些用户容易流失:
http://www.woshipm.com/pmd/155084.html

  1. dau
  2. mau
  3. 时长

内容

  1. UGC(User-created Content):用户生产内容
  2. PGC(Professionally-generated Content):专业生产内容

广告计费方式:

  1. CPC

按点击付费,原英文为Cost Per Click 每点击成本,网络广告每次点击的费用。是做为网络广告投放效果的重要参考数据。CPC是网络广告界一种常见的定价形式。例如,关键词广告等依据效果付费的广告形式,一般采用这种定价模式。

  1. CPM

按千次展示付费,其原始英文为Cost Per Mille,简称CPM。现在也有人将其译成cost per one thousand impressions 或cost per thousand。前者中文名为每千人印象成本。后者称谓相同,但简称为CPT。所指内涵与CPM相同。其计算公式为:
千人成本=(广告费用/到达人数)×1000。

  1. CPA

按行为付费,其原始英文为 Cost Per Activity(Action),每次动作成本,即根据每个访问者对网络广告所采取的行动收费的定价模式。对于用户行动有特别的定义,包括形成一次交易、获得一个注册用户、或者对网络广告的一次点击等。

  1. CPD

按天收费,其原始英文为 Cost per day是广告合作的一种常见方式,相比当前比较流行的CPS(按销售付费 Cost per sales),优势在于对合作的基础条件没有过高要求,容易促成双方合作;劣势在于其在长期合作中,不如CPS形式实时有效。

  1. CPS

按实际销售产品的提成来换算广告刊登金额,其原始英文为Cost Per Sales,CPS广告同CPA广告一样广告主为规避广告费用风险,按照广告点击之后产生的实际销售的提成付给广告站点销售提成费用。

  1. dCPM

DSP普遍采用dCPM作为结算体系,dCPM指的是dynamic CPM,与目前网络广告市场长讲的CPM方式(此CPM相应的成为flat CPM)区别。dCPM基于RTB技术诞生,指的是每一次的impression出价是变化的。其每次出价均依据广告主广告投放的效果(一般是CPS)来实时计算,以得出对广告主最有利的价格,从而保证了广告主的利益。同时又因为以impression与媒体结算,也确保了媒体的收益。

  1. eCPM
    全称earning cost per mille,指的就是每一千次展示可以获得的广告收入,普通广告的展示收入在(3~8块),医药广告会高很多

  2. KOL
    全称Key Opinion Leader,关键意见领袖。KOL被视为一种比较新的营销手段,它发挥了社交媒体在覆盖面和影响力方面的优势。KOL的粉丝黏性很强,价值观各方面都很认同他们,所以KOL的推荐,是带有光环的,粉丝们真会细读点赞。

https://www.umeng.com/reports.html
中国移动互联网发展报告 会有说明行业普遍的留存情况

主题皮肤切换

发表于 2017-10-16 | 分类于 Android

1. 一个Icon图标支持两种配色

1.1 通过已有的图片处理成变色图片

方案就是作图的时候控制显示内容的透明度,

第一步:

Bitmap mColorBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas mCanvas = new Canvas(mColorBitmap);
Paint mPaint = new Paint();
mPaint.setColor(mColor);  

第二步:

//从原位图中提取只包含alpha的位图
Bitmap alphaBitmap = mBitmap.extractAlpha();
//在画布上通过透明度的部分控制显示颜色
mCanvas.drawBitmap(alphaBitmap, 0, 0, mPaint);

ps:使用把图片做成字体,这样可以直接setTextColor
其实这和第二种的原理是相同的,只不过是UI和Android系统帮我们做了

1.2 ImageBitmap 和倍率的关系

如果不设置Density 直接把一个bitmap 设置到ImageVIew上去 就是默认density。
但是可能图片都是三倍图,这时候如果设置到2倍分辩率的手机上就会出现缩小的情况,这时候就要设置图片的density。
bitmap.setDensity(480);
然后再设置到imageView上去

关于图片的缩放系数:http://jayfeng.com/2016/03/22/Android-Bitmap%E9%9D%A2%E9%9D%A2%E8%A7%82/

2.一个轻量级的换肤方案

https://github.com/hongyangAndroid/AndroidChangeSkin

原理是定义可替换的皮肤时候带上一个后缀

<color name="bg_theme">#07172d</color>
<color name="bg_theme_light">@color/color_eeeeee</color>   

对于自定义drawble和图片资源也是一样

discover_icon.png   
discover_icon_light.png  

使用时候:

<ImageView
android:drawableTop="@drawable/discover_icon"
android:tag="skin:drawableTop:drawableTop|skin:tab_color:textColor"
/>

原理每次换肤时候遍历所有的activity,找出所有有skin标签的控件

private void notifyChangedListeners() {

    long time = System.currentTimeMillis();

    LinkedList<Activity> activityStack = LycApplication.getInstance().getActivityStack();

    for (int i = 0, size = activityStack.size(); i < size; i++) {
        if (null != activityStack.get(i)) {
            Activity act= activityStack.get(i);
            if (act instanceof StoryGroundActivity || act instanceof ReviewActivity
                    || act instanceof CommentActivity) {
                apply(activityStack.get(i));
            }
        }
    }

    ELog.w(" Notify ALL SKIN COST: ", (System.currentTimeMillis() - time));
}

找每个元素的过程:

1.递归遍历所有的子元素

public static SkinView getSkinView(View view) {
    Object tag = view.getTag(R.id.skin_tag_id);
    if (tag == null) {
        tag = view.getTag();
    }

    if (tag == null)
        return null;

    if (!(tag instanceof String))
        return null;

    String tagStr = (String) tag;

    List<SkinAttr> skinAttrs = parseTag(tagStr);
    if (!skinAttrs.isEmpty()) {
        changeViewTag(view);
        return new SkinView(view, skinAttrs);
    }
    return null;
}

2.解析出每个SkinView里面的自定义主题 SkinAttr

    private static List<SkinAttr> parseTag(String tagStr) {
    List<SkinAttr> skinAttrs = new ArrayList<SkinAttr>();
    if (TextUtils.isEmpty(tagStr)) return skinAttrs;

    String[] items = tagStr.split("[|]");
    for (String item : items) {
        if (!item.startsWith(SkinConfig.SKIN_PREFIX))
            continue;
        String[] resItems = item.split(":");
        if (resItems.length != 3)
            continue;

        String resName = resItems[1];
        String resType = resItems[2];

        SkinAttrType attrType = getSupprotAttrType(resType);
        if (attrType == null) continue;
        SkinAttr attr = new SkinAttr(attrType, resName);
        skinAttrs.add(attr);
    }
    return skinAttrs;
}

3.应用新的主题

SkinView apply主题的方法:

public void apply() {
    if (view == null) return;

    for (SkinAttr attr : attrs) {
        attr.apply(view);
    }
}

SkinAttr 具体设置主题的方法

HintCOLOR("textColorHint") {
    @Override
    public void apply(View view, String resName) {
        ColorStateList colorlist = getResourceManager().getColorStateList(resName);
        if (colorlist == null)
            return;

        ((EditText) view).setHintTextColor(colorlist);
    }
},

ps: SkinAttrType写的方式很不错,值得学习

踩坑笔录

  • 区别drawable 是color 还是 drawable:
 BACKGROUND("background") {
    @Override
    public void apply(View view, String resName) {

        String targetResName = getResourceManager().appendSuffix(resName);
        Resources mResources=view.getContext().getResources();
        try {
            int colorResult= mResources.getColor(mResources.getIdentifier(targetResName, "color", view.getContext().getPackageName()));
            view.setBackgroundColor(colorResult);
        }catch ( Exception e1){
            try {
                int resId=mResources.getIdentifier(targetResName, "drawable", view.getContext().getPackageName());
                Drawable drawableResult= mResources.getDrawable(resId);
                if (drawableResult == null)
                    return;
                view.setBackgroundResource(resId);
            }catch (Exception e2){
                ELog.e("not found:"+targetResName);
            }
        }


    }
},
  • SkinManager 没有保存SkinView的引用

实际上每次应用主题时候 都是遍历存在Application的Activity列表然后find 每个SkinView 然后应用主题皮肤

Android版本更新特性

发表于 2017-10-11 | 分类于 Android

1. Android 3.0

  • 硬件加速

2. Android 4.1

  • 黄油计划

3. Android 4.4

  • 推出art 但是默认还是davilk
  • 对齐唤醒:把AlarmManager的默认方法改用了对齐但不保证准确的模式

4. Android 5

  • Android Runtime (ART)默认运行平台设置
  • 引入Material Design设计
  • 转场动画

保活注意点:

在5.0以前使用LibMarsdeamon的保活方法
在5.0以后使用JobScheduler进行保活

JobScheduler使用系统定义要在以后的某个时间或在指定的条件下(例如,当设备在充电时)异步运行的作业来优化电池寿命

JobInfo.Builder builder = new JobInfo.Builder(job,
                        new ComponentName(ApplicationManager.ctx, CustomJobSchedulerService.class));
    builder.setPeriodic(3000);// 每隔三秒运行一次  设置太小会无效
    builder.setPersisted(true);

    JobScheduler jobScheduler = (JobScheduler) ApplicationManager.ctx.
            getSystemService(Context.JOB_SCHEDULER_SERVICE);
    if (jobScheduler!=null) {
        jobScheduler.schedule(builder.build());
    }

Android 6.0

  • 运行时请求权限
  • Doze mode:应用不在白名单,系统灭屏经过大约一小时后,上层应用wake lock,alarm,还有网络链接都会失效
    参考链接: http://blog.csdn.net/xiaorenwu1206/article/details/49358433

Android7.0

  • 分屏
  • 加强版的Doze模式:N与6.0的区别就在于N在手机非静止时也可进入低电耗模式
  • Project Svelte:后台优化

Android 8.0

  • 优化通知

参考资料

  1. https://www.jianshu.com/p/8a66806588bc
  2. Doze模式的详细解释 https://blog.csdn.net/qq_25804863/article/details/50229437 和 https://developer.android.google.cn/training/monitoring-device-state/doze-standby

点赞爱心动画

发表于 2017-10-10 | 分类于 Android

场景

实现直播房间点赞爱心动画(不是送礼物的动画)
难点:流畅的飘爱心

1. 方案1

直接使用animator做动画
参考资料: https://github.com/Yasic/QQBubbleView

1.1. 优化方向:

  1. 削峰
  2. 不要每次都inflate layout,尝试模拟listview的recycleBin 对用跑完动画的View 重新赋值初始化

2. 方案2:

直接在View上绘制爱心
参考资料:https://github.com/HomHomLin/Android-DivergeView
核心就是一个while循环,不断计算当前爱心的位置

while (mRunning) {
    if (mQueen == null) {
        continue;
    }
    if (mIsDrawing) {
        //如果正在绘制,不要处理数据
        continue;
    }
    dealQueen();
    dealDiverge();
    mIsDrawing = true;
    postInvalidate();
}

在dealQueen里面处理正在等待走动画的点赞

if (mQueen.size() > 0 && now - mLastAddTime > mQueenDuration) {
        mLastAddTime = System.currentTimeMillis();
        DivergeInfo divergeInfo = null;
        if (mDeadPool.size() > 0) {
            //死池里面有空闲的divergeNode
            divergeInfo = mDeadPool.get(0);
            mDeadPool.remove(0);
        }
        if (divergeInfo == null) {
            divergeInfo = createDivergeNode(mQueen.get(0));
        }
        divergeInfo.reset();
        divergeInfo.mType = mQueen.get(0);
        mDivergeInfos.add(divergeInfo);
        mQueen.remove(0);
    }

在dealDiverge里面计算已经在走动画的爱心的位置

for (int i = 0; i < mDivergeInfos.size(); i++) {
       DivergeInfo divergeInfo = mDivergeInfos.get(i);
       float timeLeft = 1.0F - divergeInfo.mDuration;
       divergeInfo.mDuration += mDuration;
       float x, y;
       //二次贝塞尔
       float time1 = timeLeft * timeLeft;
       float time2 = 2 * timeLeft * divergeInfo.mDuration;
       float time3 = divergeInfo.mDuration * divergeInfo.mDuration;
       x = time1 * (mPtStart.x)
               + time2 * (divergeInfo.mBreakPoint.x)
               + time3 * (divergeInfo.mEndPoint.x);

       divergeInfo.mX = x;

       y = time1 * (mPtStart.y)
               + time2 * (divergeInfo.mBreakPoint.y)
               + time3 * (divergeInfo.mEndPoint.y);

       divergeInfo.mY = y;

       if (divergeInfo.mY <= divergeInfo.mEndPoint.y) {
           mDivergeInfos.remove(i);
           mDeadPool.add(divergeInfo);
           i--;
           continue;
       }
   }

onDraw方法

for (DivergeInfo divergeInfo : mDivergeInfos) {
          mPaint.setAlpha((int) (255 * divergeInfo.mY / mPtStart.y));
          Bitmap bm = mDivergeViewProvider.getBitmap(divergeInfo.mType);
          float originCenterX = divergeInfo.mX + bm.getWidth() / 2;
          float originCenterY = divergeInfo.mY + bm.getHeight() / 2;
          float scaleWidth, scaleHeight;               
          scaleWidth = bm.getWidth();
          scaleHeight = bm.getHeight();
          RectF destRect = new RectF(originCenterX - scaleWidth / 2, originCenterY - scaleHeight / 2,
                  originCenterX + scaleWidth / 2, originCenterY + scaleHeight / 2);
          canvas.drawBitmap(bm, null
                  , destRect,
                  mPaint);
      }
 mIsDrawing = false;

ps:映客的点赞爱心就是这么实现的(映客是本地点赞出爱心,这个直接分析元素会失败,可以进入直播后,断网然后分析元素)

2.1. 优化方向:

  1. 停止爱心动画的时机(什么时候可以停止while循环)
  2. 削峰:Android 每隔 16.6 ms 刷新一次屏幕,可以根据这个计算一次主流机型阈值

都需要利用到削峰

额外知识点

二阶贝塞尔曲线公式:
二阶

三阶贝塞尔曲线公式:
三阶

在线演示:http://myst729.github.io/bezier-curve/

热补丁

发表于 2017-10-01 | 分类于 Android

核心两个部分

1. hook部分

hook部分主要是使用反射调用补丁里面的内容

hook部分的本质是提供对补丁方法调用的封装(主工程无法直接实例和调用补丁的方法),每次调用都是通过反射调用,传入activity,handler,callback进去。

2. 补丁部分

补丁实际上是java代码转的dex
它的工作有两种:网络取数据,构造生成view

补丁部分构造view是通过hook部分传过来activity,然后用代码动态构造view,不涉及到任何布局文件,资源文件。

hook的初始化及调用过程

  1. 初始化DexClassLoader

    classLoader = new DexClassLoader(dexApkFilePath
                + dexName + ".apk", context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null,
                context.getClassLoader().getParent());
    
  2. load补丁相应的class

    controllerClass = classLoader.loadClass("com.nico.Controller");
    
  3. 初始化构造函数

    controllerConstrucor = controllerClass.getConstructor(new Class[]{Context.class, String.class, boolean.class});
    
  4. 实例化补丁里面的类

    controllerInstance = controllerConstrucor.newInstance(new Object[]{mContext});
    
  5. 通过初始化的实例controllerInstance来调用里面的方法

    Method initController = controllerClass.getDeclaredMethod("initController", new Class[]{String.class, String.class, String.class});
    initController.setAccessible(true);
    initController.invoke(controllerInstance, new Object[]{paramsA, paramsB, paramsC});
    

补丁的升级

  1. 校验是否有新的版本升级
  2. 把新的补丁下载到本地sd卡中去
  3. 解压补丁到data/data/app.pkg/app_dex目录
    ZipManager.extNativeZipFile(mContext,
    mContext.getResources().getAssets().open(Utils.ZIP_NAME),
    cachePath, dexApkFilePath, lastVersion)
    
  4. 重新走一遍补丁的实例化过程(从DexLoader开始)

坑

  1. 第一次安装,补丁放到asset目录下面
  2. 由于里面传入了activity,要注意销毁补丁里面的强引用

其他

dexclassloader : 可以加载apk文件中的字节码
pathclassloader : 只能加载文件目录下的apk文件中的classes

To Be Continue

  1. load补丁里面的资源文件 ,通过反射调用AssetManger里面的资源文件,把ID设置上去

    AssetManager assetManager = AssetManager.class.newInstance();  
    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
    addAssetPath.invoke(assetManager, libPath);  
    Resources superRes = super.getResources();  
    mRes = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
    
  2. 尝试使用mvp模式,把controllor从activity里面剥离开来,让所有contollor可以被热补丁,这要处理起来(可以把整个app轻量化)

参考资料

http://blog.csdn.net/wwj_748/article/details/46349781

http://blog.csdn.net/yuanzeyao/article/details/42390431

http://blog.csdn.net/u010386612/article/details/51077291

http://blog.csdn.net/cn_foolishman/article/details/46874811

全屏模式底部键盘冲突

发表于 2017-09-10 | 分类于 Android

场景:全屏模式下activity的adjustResize 失效,弹键盘时候底部的输入框无法自动调整到键盘区域的上方

1. 方案一:

使用 AndroidBug5497Workaround (https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible)

原理:通过 getViewTreeObserver().addOnGlobalLayoutListener监听当前屏幕显示区域的大小,然后动态调整Activity rootView的height,实现“adjust resize”的效果

 Rect r = new Rect();
 mChildOfContent.getWindowVisibleDisplayFrame(r);
int usableHeightNow = r.bottom - r.top;
  if (usableHeightNow != usableHeightPrevious) {
      int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
      int heightDifference = usableHeightSansKeyboard - usableHeightNow;
      if (heightDifference > (usableHeightSansKeyboard/4)) {
          // keyboard probably just became visible
          frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
      } else {
          // keyboard probably just became hidden
          frameLayoutParams.height = usableHeightSansKeyboard;
      }
      mChildOfContent.requestLayout();
      usableHeightPrevious = usableHeightNow;

但是这个不能完全解决我们问题,会遇到下面的问题:
在输入框在屏幕区域上方是没问题,在输入框在底部时候,会有一个特别不好的现象,键盘弹出后会迅速把整个Activity完全顶上去,然后调整回来。

问题的原因是是OnGlobalLayoutListener不是实时和键盘弹时屏幕显示区域同步的。所以就会在键盘弹起的瞬间先把整个activity顶上去,然后顶上去后,屏幕内容又会掉下来

补丁方案:
输入框在底部时候不要直接弹出键盘,先让顶部的一个1px的输入框获得这个这个焦点,待键盘完全弹出来后再把焦点传递给底部的这个输入框,这样就不会有顶整个界面起来的现象

补充问题
为什么在顶部的时候不会出现顶上去又下来的情况?
因为 输入框在最上面,键盘输入法只要弹出时不盖住输入框,就不会顶起这个Activity的内容,而内容区域调整也是延迟于键盘弹起速度的,但是由于键盘盖住了所以视觉感觉不出来

虚拟按键

注意android的虚拟按键有两种:
一种是N5 N6一类的固定在底部的虚拟按键

一种是华为 小米mix 一类的的 可以通过手势动态控制底部虚拟按键显示还隐藏
这种就需要动态计算高度

为了解决A问题,需要在顶部放置一个fake EditText ,点击目标EditText时候先让顶部的fakeEditText获得焦点,待键盘完全弹出来后再把焦点转移给目标EditText
具体方案:目标EditText监控ontouch事件,在TouchUp时候消耗掉这个事件,这样就不会触发键盘直接弹出来了,这时候,顶部的 fake EditText先获取焦点,延迟一段时间 待键盘完全弹出后再让目标EditText获取焦点

2.方案二

JkeyboardSwitch (https://github.com/Jacksgong/JKeyboardPanelSwitch)

原理是在输入框下面加了一个和键盘高度相同的panel,在键盘弹出来的瞬间panel显示出来(invisualble状态),这样键盘的弹出和隐藏与panel的的invisualble和gone的状态同步起来,看起来的效果就是键盘出来时候把输入框顶上去,键盘收起来时候把输入框收回去

这个方案的一个问题是“怎么在第一次弹出时候初始化panel高度和键盘高度一致?”

补丁方案:
第一次弹出瞬间把输入框的区域设置为Invisualbel ,初始panel的高度尽量大,待键盘完全弹出后把输入框区域显示出来,然后调整panel的高度,这样就能在用户无察觉的情况初始化这个panel高度。

又拍云图片显示优化

发表于 2017-08-10 | 分类于 Android

1. 又拍云的图片尺寸问题

又拍云会对我们上传的图片进行处理。然后会生成多种尺寸:110,160,210,240,320,480,640,720,1200。
每种尺寸对应于不同的url。
eg:
http://img2048.static.suishenyun.net/d62bb1e1bf1e2c6b8fb0dd004a952a01/4414fc28822e3643ff90a7e68305e12a.jpg!w480.jpg

2. 简单的解决方案

为了利用又拍云自动帮我们处理出来的不同尺寸图片。在每次加载图片的时候根据当前显示View的尺寸加载合适的尺寸

2.1 这里有三个没解决的问题:

  1. 不同尺寸的图片会在sd卡上存不同cache,比如由于我们先加载了640尺寸的图片,后面加载480尺寸还是无法公用640尺寸的图片cache,导致还得重新请求一次网络。实际上我们可以通过处理得到480尺寸的图片。

  2. 对于发带图片帖子的这种逻辑处理逻辑处理不方便。因为上传图片后,很有可能不知道图片的显示尺寸。无法把这张本地图片放置到对应位置的cache上去(虽然我们现在上传上去后,会直接显示的是本地图片,但这个有点治标不治本。第二次进来时候或者下拉刷新后 还是会从网上load这张图片)。

  3. 图片尺寸的获取,不是所有的ImageView在setImageUrl时候都可以获取尺寸,有的量不到尺寸,也就是在一个还没有渲染 并且也没有设置固定size的ImageVIew上设置Url(比Adapter里面的初次createView 然后立即设置Url就会出现量不到尺寸的问题)

总结一些:又拍云会帮我们处理出来不同尺寸的图片。在小的地方我们可以仅仅加载小尺寸的图片,提高了展示速度。但是对于同一张图片,在不同地方展示不同尺寸的逻辑处理不够完美。所以以前在wecal里面会发现一个问题:同一个头像,已经展示过了,在下一个页面里用另个尺寸展示的时候还是会有点慢。

3. 对于又拍云的图片完美解决方案

核心需要在内存和sd卡缓存逻辑能够识别出不同尺寸的不同图片。也就是要每一url对应于一组cache。

代码实现上就是要把当前的url的cache对应到一组cache 上去。

3.1 Memery cache逻辑

对于内存cache 建议还是按照以前的逻辑走,但是可以加上对于一个尺寸的的图片可以使用相近已经缓存尺寸的图片。

3.2 Disk cache逻辑

对于Disk cache 。如果需要加载一个小尺寸的图片,但是发现只有大尺寸图片,我们可以直接使用大图压缩出小图,返回过去。

3.3 2G网络模式

在低速网络情况下,对于disk cache,如果发现没有当前尺寸的图片的情况下,可以使用任意尺寸的缓存转化。

5. 推荐一个图片压缩库

Luban:https://github.com/Curzibn/Luban 大小仅仅是15K

根据我的测试一般可以在不明显影响图片展示效果的条件减少图片体积2/3。使用它后发图片备注速度飞快。(在wecal里面默认发送压缩后的图片,也提供发送原图的可选项)

相关推荐

Glide的ModuleLoader:https://github.com/bumptech/glide/wiki/Downloading-custom-sizes-with-Glide
Android图片缓存之Glide进阶: http://www.cnblogs.com/whoislcj/p/5565012.html
ModelLoader: https://muyangmin.github.io/glide-docs-cn/tut/custom-modelloader.html#%E5%9C%A8-modelloader-%E4%B8%AD%E5%A4%84%E7%90%86%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B0%BA%E5%AF%B8

https://github.com/bumptech/glide/blob/b4b45791cca6b72345a540dcaa71a358f5706276/samples/giphy/src/main/java/com/bumptech/glide/samples/giphy/GiphyModelLoader.java#L31

AMS server端分析

发表于 2017-08-03 | 分类于 Android

Ams server端

核心类:

ActivityManagerNative是从Client接受binder 消息
ApplicationThreadNative 是从AMS向Client发送指令
ActivityStackSupervisor
ActivityStack就是存储ActivityRecord的容器,主要操作都是通过ActivityStackSupervisor来做的

ps:注意接IApplicationThread

A. ApplicationThreadNative实现了一部分,ActivityMangerService继承ApplicationThreadNative,然后实现了剩余的    
B. ApplicationThreadNative.ApplicationThreadProxy完全实现了IApplicationThread

Activity A启动Activity B流程

1. Activity A启动Activity B流程

ActivityManagerNative#onTransact()>>
ActivityManagerService#startActivity()>>
ActivityManagerService#startActivityAsUser()>>
ActivityStackSupervisor#startActivityMayWait()>>
ActivityStack#resumeTopActivityLocked()>>
ActivityStack#startPausingLocked()>>
pause上一个Activity的调用方法

prev.app.thread.schedulePauseActivity(prev.appToken, prev.finishing,  userLeaving, prev.configChangeFlags);

ActivityMangerNative.ApplicationThreadProxy.schedulePauseActivity >>
)

等client pause完了后
ActivityManagerNative#activityPaused()
这里实际调用的是ActivityManagerService的activityPaused()>>
ActivityStack#completePauseLocked() >>
ActivityStack#resumeTopActivityLocked()>>
ActivityStackSupervisor#resumeTopActivitiesLocked(topStack, prev, null);

这时候有两部分,如果当前应用还没有启动就通过Process

    if (app != null && app.thread != null) {
        try {
            app.addPackage(r.info.packageName, mService.mProcessStats);
            realStartActivityLocked(r, app, andResume, checkConfig);
            return;
        } catch (RemoteException e) {
            Slog.w(TAG, "Exception when starting activity "
                    + r.intent.getComponent().flattenToShortString(), e);
        }

        // If a dead object exception was thrown -- fall through to
        // restart the application.
    }

    mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
            "activity", r.intent.getComponent(), false, false, true);
}

1.1 在已有进程中启动

ActivityStackSupervisor#realStartActivityLocked()>>

app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
System.identityHashCode(r), r.info,
new Configuration(mService.mConfiguration), r.compat,
app.repProcState, r.icicle, results, newIntents, !andResume,
mService.isNextTransitionForward(), profileFile, profileFd,profileAutoStop;  

ActivityMangerNative.ApplicationThreadProxy.scheduleLaunchActivity >>

1.2 App进程不存在,需要新建

ActivityManagerService#startProcessLocked()>>

1.2.1 fork一个新的进程的三个步骤:

①AMS通过Socket通信,向Zygote发送一个创建进程请求,Zygote创建新进程。
②创建好进程后,调用ActivityThread.main()。到此,我们到了新了一个进程中,也是程序的入口出。
③调用ActivityThread.attach()开始新的应用程序,接着同过Binder通信通知AMS,新的进程已经创建好了,可以开始新的程序了。

1.2.2 ActivityManagerNative.attachApplication()

ps:实际调用的是ActivityManagerService.attachApplication
①根据Binder.getCallingPid(),或得客户进程pid,并调用attachApplicationLocked(IApplicationThreadthread,int pid)
②在attachApplicationLocked中,根据pid找到对应的ProcessRecord对象,如果找不到说明改pid客户进程是一个没经过AMS允许的进程。
③为ProcessRecordapp对象内部变量赋值
④确保目标程序(APK)文件已经被转换为了odex文件。Android中安装程序是APK文件,实际上是一个zip文件。
⑤调用ActivityStack.realStartActivityLocked通知客户进程运行指定Activity.
⑥调用ApplicationThread.scheduleLaunchActivity,启动指定Activity。

1.2.3 客户进程启动指定Activity

AMS通过IPC通行,通知客户进程启动指定Activity:
①调用ApplicationThread.scheduleLaunchActivity
②经过Handler消息传动,调用ActivityThread.handleLaunchActivity()
③调用ActivityThread.performLaunchActivity()完成Activity的加载,并最终调用Activity生命周期的onCreate()方法
④performLaunchActivity返回,继续调用ActivityThread.handleResumeActivity(),该方法内部又调用ActivityThread.performResumeActivity(),其内部仅仅调用了目标Activity的onResume()方法。到此Activity启动完成。
⑤添加一个IdleHandler对象,因为在一般情况下,该步骤执行完毕后,Activity就会进入空闲状态,所以就可以进行内存回收。

2. ActivityStack 、TaskRecord和ActivityRecord

从ActivityStack#destroyActivityLocked()>>

ActivityStack#removeActivityFromHistoryLocked()>>

final TaskRecord task = r.task;
if (task != null && task.removeActivity(r)) {
    if (DEBUG_STACK) Slog.i(TAG,
             "removeActivityFromHistoryLocked: last activity removed from " + this);
     if (mStackSupervisor.isFrontStack(this) && task == topTask() && task.mOnTopOfHome) {
          mStackSupervisor.moveHomeToTop();
      }
       mStackSupervisor.removeTask(task);
  }
 r.takeFromHistory()

这里正式开始处理TaskRecord栈里面Activity记录

AMS client端分析

发表于 2017-08-01 | 分类于 Android

Activity启动理解

PS:
一定要下载官方对应版本SDK,然后应用的SDK版本也要对应
建议配合GrepCode 查找类的位置,

1. Android的CS模式

Android启动是通过AtivityThread的main函数启动的,
四大组件的生命周期都是通过AMS远程控制的,比如我们启动一个Acitivity,Service等都不是直接启动的,都是binder告诉AMS我们要启动四大组件,然后AMS通过binder处理(比如Activity A要启动Activity B,但是中途涉及到A的onpause onStop和B 的oncreate,都是AMS依据ActivityRecord进行处理的)

2. 三个核心部分

client端:

ActivityThread
ActivityMangerNative 和ActivityManagerProxy
ApplicationThread和ApplicationThreadNative

其实还涉及到:其它的比如 Instrumentation但是这个不是核心类

Server端

ApplicationThreadProxy
ActivityMangerService
ProcessRecord
ActivityStack
ActivityRecord

跨进程通讯Binder

Binder 和 Parcel

3. Activity A 启动Activity B的流程详解

ps:
ActivityMangerNative是往AMS发送消息的
ApplicationThread是从AMS接受指令消息的

1、Client发送Intent

Activity#startActivity(Intent) >> Instrumentation#execStartActivity() >>
ActivityManagerNative#startActivity() >>

2、AMS 查询栈,告诉要Pause

ActivityMangerNative#onTransact() >>

ActivityMangerNative#schedulePauseActivity()这里是抽象的具体实现在
ActivityThread$ApplicationThread#schedulePauseActivity() >>

ActivityThread#sendMessage() 也就是通过H mH 发送Message>>
ActivityThread$H#handleMessage()>>
ActivityThread#handlePauseActivity这里先要取出当前Activity ActivityClientRecord r = mActivities.get(token) >>
ActivityThread#performPauseActivity()>>

Instrumentation#callActivityOnPause >>

ActivityManagerNative#activityPaused(token) 这个是在上一步Instrumentation pause完Activity后顺序执行的
这里也就是通过binder发送消息>>

PS:Activity切换要不要调用onstop 要看Theme,如果是android:style/Theme.Translucent就不会调用Onstop。所以我是不讲stop这个流程(实际上鄙人是有点懒。。。)

3、AMS正式启动 Activity B

ActivityMangerNative#onTransact() >>

ActivityMangerNative#scheduleLaunchActivity()这里是抽象的具体实现在
ActivityThread$ApplicationThread#scheduleLaunchActivity() >>

经过H mH转到主线程里面
ActivityThread#handleLaunchActivity>>

ActivityThread#performLaunchActivity
初始化Activity包括以下几步

  1. 生成Activity B ,这里仅仅是生成简单的java类

    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
             activity = mInstrumentation.newActivity(
                     cl, component.getClassName(), r.intent);  
    
  2. 附加给Activity 各种Android系统类型

    attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor) 
    
  3. 回调oncreate 注意这个地方不会通知AMS

    mInstrumentation.callActivityOnCreate(activity, r.state);  
    

最终这个会调用到Activity的oncreate

ps:ActivityThread.H 的Message可以看看,就可以知道有哪些操作需要和AMS打交道

ActivityThread#handleResumeActivity
这里会依次调用

  1. ActivityThread#performResumeActivity(token, clearHide)
  2. ActivityManagerNative#willActivityBeVisible()
    告诉AMS Activity已经准备好了
  3. ActivityManagerNative#activityResumed()
    告诉AMS Activity已经resumed

在2、3步骤里面就是做Activity B的界面绘制工作:

r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
    a.mWindowAdded = true;
    wm.addView(decor, l);
    }

具体流程参考ActivityThread#handleResumeActivity()流程

涉及到window,DecroView等

AMS 通知显示Acitivity B已经Onresume,

然后就是和onpause一样的流程并且回调Activity的onResume

4. 哪些是公用的,为hook做准备

1234
Liuycheng

Liuycheng

34 日志
8 分类
34 标签
© 2018 Liuycheng
由 Hexo 强力驱动
主题 - NexT.Muse