Android 安卓腾讯直播一小时快速集成(无需后台配合一小时让你拥有直播APP)

前言:

最近公司需要开发一个直播互动的功能,最终选择了腾讯云平台进行开发,因为腾讯强大的实力,LiveRoom组件里面包含了(直播,连麦,弹幕,私信等)功能,我们需要的是推流,拉流都交个腾讯云来处理,腾讯云这方面功能也是比较齐全的了(粗略大概2分钟可看完本文,能像我写如此详细的全网暂时木有可以说,如果你是第一次接触直播可谓福音不敢说,无需自己后台一小时让你拥有直播app不在话下!文末有巨型福利相送及手写FFmpeg推流拉流教程源码+高清视频)

而且腾讯云直播也是按照流量收费的,没有其他任何费用,所以如果要使用腾讯云直播+IM聊天(基本免费按日活收费)功能的,最好还是全部选用腾讯一家即可。

网上相关的文章很少,而且大厂的文档虽然高级但看起来集成过程还是有点慢的,

我这篇文章主要是手把手详细介绍Android 快速集成腾讯移动直播,无需后台任何配合我们前端直接调试成功!看完本文大概十分钟,半天搞定整个直播互动连麦。集成路上大概可以帮助大家节省一天时间,希望可以帮助到各位.

下载SDK

1.功能不同多种版本可以选择,一般来说,选择全功能专业版就好了.
官网集成到AS步骤https://cloud.tencent.com/document/product/454/7877
个人用你 qq号登录后即可下载,
另外开通直播云服务:https://cloud.tencent.com/document/product/267/13551
以及IM云通信功能:https://console.cloud.tencent.com/avc 创建应用,点击购买开通一元基础版即可满足调试。

image.png
image.png

下载SDK后首先,运行Demo源码,跑通源码直接看到效果是最好的。建议先在demo源码上改动来实现调试成功。

image.png

运行Domo项目后你可以试试可以在线看到同样使用本SDK demo 的用户在上面调试,你只需要把项目里的sdkAppId 和直播云appId 替换到你自己的项目,就可以替换成你再腾讯云开通的直播服务和使用你刚刚创建的项目的Im聊天连麦功能了。但是这里面还是有一些步骤的,不是简单的替换就可以的。

首先我建议你用debug的方式,在点击美女直播,到点击直播列表,到进入直播聊天室,以及新建直播间,这一部分多打一些断点,你就大概知道带直播连麦的直播聊天功能大致流程是:登录聊天室--获取推流地址--直播。 这里的登录是必须的步骤,直播和云通信是互通的,你可以用这一行代码来检测是否登录云通信成功:String loginUser = TIMManager.getInstance().getLoginUser();有返回值说明已经IMSDk初始化登录成功了。

如果你仔细想先用demo测试下 推流 拉流是否成功,那么可以先配置好播放域名,然后利用demo里的 首页---调试工具---RTMP推流,将你的在腾讯云直播生成推流地址出复制--粘贴到输入框--点击底部开始按钮--查看是否推流成功--log,然后电脑下载VLC播放器,打开---网络流播放---将的在腾讯云直播生成播放地址粘贴---播放,即可测试是否推流,拉流成功。具体见下面步骤:

播放域名设置https://console.cloud.tencent.com/live/domainmanage

如下图,一定是绿色标记显示了,才说明你的域名设置成功并cname成功了,否则是不会播放成功的,这里要是你自己的备案的域名,如果你没有可以阿里云购买,然后申请备案,可能需要你购买一个云服务器才会送备案号,有了域名后解析是需要注意,如果你的是域名比如是abc.cn 阿里云的解析设置里 记录类型请填@,记录值亲填腾讯后台记录值那里给你自动生成的那一串全部。如果你的是www.abc.cn 那么记录类型填www 否则是不会成功的哦。配置后可以ping下是能看到腾讯的服务器地址就算配置ok了。但是一定时要下图中的绿色标记显示了才算完全正常哦!

image.png

image.png

推流,拉流地址生成https://console.cloud.tencent.com/live/livecodemanage
推流域名可以先不设置自己的,默认已经给你生成了。至此你如果你不需要互动连麦的功能,只需要用腾讯的点播播放器就可以直接播放你生成的播放观看实时直播了。

后台对接腾讯云

我们推流,拉流都是交给腾讯云处理的,
LiveRoom 为什么需要 login?

LiveRoom 单靠一个终端的组件无法独自运行,它依赖一个后台服务为其实现房间管理和状态协调,这个后台服务我们称之为房间服务(RoomService)。而要使用这个房间服务,LiveRoom 就需要先进行登录(login)。

login 有很多参数需要填写,我应当如何填写这些参数呢?

如下表格中列举了三种填写方案,每种方案都有其适用场景:方案一适合调试;方案二适合快速上线;方案三适合自行定制;
你可有看下图:
https://cloud.tencent.com/document/product/454/14606

image.png

image.png

login(serverDomain, sdkAppID, accType, userID, userSig),可以看到登录索要的参数,accountType 是云通信后台你创建项目IM 配置那里可以生成的,userId是你的真实项目你的用户userId用户唯一标识即可,只有userSig 是需要后台生成的,但是这个我们调试阶段可以在客户端生成方便是一样的,
你也可以直接复制下面的生成userSig ,

package com.tencent.liteav.demo.webrtc;

import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.Charset;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.zip.Deflater;

public class WebRTCSigApi {
    private int mSdkAppid = 0;
    private PrivateKey mPrivateKey = null;
    private PublicKey mPublicKey = null;
    
    /**
     * 设置sdkappid
     * @param sdkappid
     */
    public void setSdkAppid(int sdkappid) {
        this.mSdkAppid = sdkappid;
    }
    
    /**
     * 设置私钥 如果要生成userSig和privateMapKey则需要私钥
     * @param privateKey 私钥文件内容
     */
    public void setPrivateKey(String privateKey) {
        String privateKeyPEM = privateKey.replace("-----BEGIN PRIVATE KEY-----", "")
        .replace("-----END PRIVATE KEY-----", "")
        .replace("\r\n", "");
        char[] chars = privateKeyPEM.toCharArray();
        byte[] encodedKey = Base64.decode(chars);//Base64.getDecoder().decode(privateKeyPEM.getBytes());
        
        try {
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey);
            KeyFactory keyFactory = KeyFactory.getInstance("EC");
            this.mPrivateKey = keyFactory.generatePrivate(keySpec);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 设置公钥 如果要验证userSig和privateMapKey则需要公钥
     * @param publicKey 公钥文件内容
     */
    public void setPublicKey(String publicKey) {
        String publicKeyPEM = publicKey.replace("-----BEGIN PUBLIC KEY-----", "")
        .replace("-----END PUBLIC KEY-----", "")
        .replace("\n", "");
        byte[] encodedKey = Base64.decode(publicKeyPEM.toCharArray());//Base64.getDecoder().decode(publicKeyPEM.getBytes());
        
        try {
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encodedKey);
            KeyFactory keyFactory = KeyFactory.getInstance("EC");
            this.mPublicKey = keyFactory.generatePublic(keySpec);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * ECDSA-SHA256签名
     * @param data 需要签名的数据
     * @return 签名
     */
    public byte[] sign(byte[] data) {
        try {
            Signature signer = Signature.getInstance("SHA256withECDSA");
            signer.initSign(this.mPrivateKey);
            signer.update(data);
            return signer.sign();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    /**
     * 验证ECDSA-SHA256签名
     * @param data 需要验证的数据原文
     * @param sig 需要验证的签名
     * @return true:验证成功 false:验证失败
     */
    public boolean verify(byte[] data, byte[] sig) {
        try {
            Signature signer = Signature.getInstance("SHA256withECDSA");
            signer.initVerify(this.mPublicKey);
            signer.update(data);
            return signer.verify(sig);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
    
    /**
     * 用于url的base64encode
     * '+' => '*', '/' => '-', '=' => '_'
     * @param data 需要编码的数据
     * @return 编码后的base64数据
     */
    private byte[] base64UrlEncode(byte[] data) {
        char[] encode1 = Base64.encode(data);
        byte[] encode =new String(encode1).getBytes();
//        byte[] encode = Base64.encode(data);//Base64.getEncoder().encode(data);
        for (int i = 0; i < encode.length; i++) {
            if (encode[i] == '+') {
                encode[i] = '*';
            } else if (encode[i] == '/') {
                encode[i] = '-';
            } else if (encode[i] == '=') {
                encode[i] = '_';
            }
        }
        return encode;
    }
    
    /**
     * 用于url的base64decode
     * '*' => '+', '-' => '/', '_' => '='
     * @param data 需要解码的数据
     * @return 解码后的数据
     */
    private byte[] base64UrlDecode(byte[] data) {
        byte[] encode = Arrays.copyOf(data, data.length);
        for (int i = 0; i < encode.length; i++) {
            if (encode[i] == '*') {
                encode[i] = '+';
            } else if (encode[i] == '-') {
                encode[i] = '/';
            } else if (encode[i] == '_') {
                encode[i] = '=';
            }
        }
        return encode;
    }
    
    /**
     * 生成userSig
     * @param userid 用户名
     * @param expire userSig有效期,出于安全考虑建议为300秒,您可以根据您的业务场景设置其他值。
     * @return 生成的userSig
     */
    public String genUserSig(String userid, int expire) {
        String time = String.valueOf(System.currentTimeMillis()/1000);
        String serialString =
        "TLS.appid_at_3rd:" + 0 + "\n" +
        "TLS.account_type:" + 0 + "\n" +
        "TLS.identifier:" + userid + "\n" +
        "TLS.sdk_appid:" + this.mSdkAppid + "\n" +
        "TLS.time:" + time + "\n" +
        "TLS.expire_after:" + expire +"\n";
        
        byte[] signBytes = sign(serialString.getBytes(Charset.forName("UTF-8")));
        char[] encode = Base64.encode(signBytes);
        String sig = new String(encode);
//        String sig = Base64.encode(signBytes);//Base64.getEncoder().encodeToString(signBytes);
        
        String jsonString = "{"
        + "\"TLS.account_type\":\"" + 0 +"\","
        +"\"TLS.identifier\":\"" + userid +"\","
        +"\"TLS.appid_at_3rd\":\"" + 0 +"\","
        +"\"TLS.sdk_appid\":\"" + this.mSdkAppid +"\","
        +"\"TLS.expire_after\":\"" + expire +"\","
        +"\"TLS.sig\":\"" + sig +"\","
        +"\"TLS.time\":\"" + time +"\","
        +"\"TLS.version\": \"201512300000\""
        +"}";
        
        //compression
        Deflater compresser = new Deflater();
        compresser.setInput(jsonString.getBytes(Charset.forName("UTF-8")));
        
        compresser.finish();
        byte [] compressBytes = new byte [512];
        int compressBytesLength = compresser.deflate(compressBytes);
        compresser.end();
        String userSig = new String(base64UrlEncode(Arrays.copyOfRange(compressBytes, 0, compressBytesLength)));
        
        return userSig;
    }
    
    /**
     * 生成privateMapKey
     * @param userid 用户名
     * @param roomid 房间号
     * @param expire privateMapKey有效期,出于安全考虑建议为300秒,您可以根据您的业务场景设置其他值。
     * @return 生成的privateMapKey
     */
    public String genPrivateMapKey(String userid, int roomid, int expire) {
        String time = String.valueOf(System.currentTimeMillis()/1000);
        
        //视频校验位需要用到的字段
        /*
         cVer    unsigned char/1 版本号,填0
         wAccountLen unsigned short /2   第三方自己的帐号长度
         buffAccount wAccountLen 第三方自己的帐号字符
         dwSdkAppid  unsigned int/4  sdkappid
         dwRoomId    unsigned int/4  群组号码
         dwExpTime   unsigned int/4  过期时间 (当前时间 + 有效期(单位:秒,建议300秒))
         dwPrivilegeMap  unsigned int/4  权限位
         dwAccountType   unsigned int/4  第三方帐号类型
         */
        int accountLength = userid.length();
        int offset = 0;
        byte[] bytes = new byte[1+2+accountLength+4+4+4+4+4];

        //cVer
        bytes[offset++] = 0;

        //wAccountLen
        bytes[offset++] = (byte)((accountLength & 0xFF00) >> 8);
        bytes[offset++] = (byte)(accountLength & 0x00FF);
        
        //buffAccount
        for (; offset < 3 + accountLength; ++offset) {
            bytes[offset] = (byte)userid.charAt(offset - 3);
        }

        //dwSdkAppid
        bytes[offset++] = (byte)((this.mSdkAppid & 0xFF000000) >> 24);
        bytes[offset++] = (byte)((this.mSdkAppid & 0x00FF0000) >> 16);
        bytes[offset++] = (byte)((this.mSdkAppid & 0x0000FF00) >> 8);
        bytes[offset++] = (byte)(this.mSdkAppid & 0x000000FF);
        
        //dwAuthId
        long nRoomId = Long.valueOf(roomid);
        bytes[offset++] = (byte)((nRoomId & 0xFF000000) >> 24);
        bytes[offset++] = (byte)((nRoomId & 0x00FF0000) >> 16);
        bytes[offset++] = (byte)((nRoomId & 0x0000FF00) >> 8);
        bytes[offset++] = (byte)(nRoomId & 0x000000FF);
        
        //dwExpTime
        long expiredTime = Long.valueOf(time) + expire;
        bytes[offset++] = (byte)((expiredTime & 0xFF000000) >> 24);
        bytes[offset++] = (byte)((expiredTime & 0x00FF0000) >> 16);
        bytes[offset++] = (byte)((expiredTime & 0x0000FF00) >> 8);
        bytes[offset++] = (byte)(expiredTime & 0x000000FF);

        //dwPrivilegeMap     
        bytes[offset++] = (byte)((255 & 0xFF000000) >> 24);
        bytes[offset++] = (byte)((255 & 0x00FF0000) >> 16);
        bytes[offset++] = (byte)((255 & 0x0000FF00) >> 8);
        bytes[offset++] = (byte)(255 & 0x000000FF);
        
        //dwAccountType
        bytes[offset++] = (byte)((0 & 0xFF000000) >> 24);
        bytes[offset++] = (byte)((0 & 0x00FF0000) >> 16);
        bytes[offset++] = (byte)((0 & 0x0000FF00) >> 8);
        bytes[offset++] = (byte)(0 & 0x000000FF);


        char[] encode = Base64.encode(bytes);
        String userbuf = new String(encode);
//        String userbuf = Base64.getEncoder().encodeToString(bytes);//Base64.getEncoder().encodeToString(bytes);
        
        String serialString =
        "TLS.appid_at_3rd:" + 0 + "\n" +
        "TLS.account_type:" + 0 + "\n" +
        "TLS.identifier:" + userid + "\n" +
        "TLS.sdk_appid:" + this.mSdkAppid + "\n" +
        "TLS.time:" + time + "\n" +
        "TLS.expire_after:" + expire +"\n" +
        "TLS.userbuf:" + userbuf + "\n";
        
        byte[] signBytes = sign(serialString.getBytes(Charset.forName("UTF-8")));

        char[] encodes = Base64.encode(signBytes);
        String sig = new String(encodes);
//        String sig = Base64.getEncoder().encodeToString(signBytes);//Base64.getEncoder().encodeToString(signBytes);
        
        String jsonString = "{"
        +"\"TLS.appid_at_3rd\":\"" + 0 +"\","
        +"\"TLS.account_type\":\"" + 0 +"\","
        +"\"TLS.identifier\":\"" + userid +"\","
        +"\"TLS.sdk_appid\":\"" + this.mSdkAppid +"\","
        +"\"TLS.expire_after\":\"" + expire +"\","
        +"\"TLS.sig\":\"" + sig +"\","
        +"\"TLS.time\":\"" + time +"\","
        +"\"TLS.userbuf\":\"" + userbuf +"\","
        +"\"TLS.version\": \"201512300000\""
        +"}";
        
        //compression
        Deflater compresser = new Deflater();
        compresser.setInput(jsonString.getBytes(Charset.forName("UTF-8")));
        
        compresser.finish();
        byte [] compressBytes = new byte [512];
        int compressBytesLength = compresser.deflate(compressBytes);
        compresser.end();
        String privateMapKey = new String(base64UrlEncode(Arrays.copyOfRange(compressBytes, 0, compressBytesLength)));
        
        return privateMapKey;
    }
    
    
  /*  public static void main(String[] args) {
        int sdkappid = 140xxxxx;   //腾讯云云通信你创建的sdkappid 
        int roomid = 1234;           //音视频房间号先随便填调试roomid
        String userid = "webrtc98";  //用户名userid 先随便填调试
        
        File privateKeyFile = new File("private_key");
        byte[] privateKey = new byte[(int)privateKeyFile.length()];
        
        File publicKeyFile = new File("public_key");
        byte[] publicKey = new byte[(int)publicKeyFile.length()];
        
        try {
            //读取私钥的内容
            //PS:不要把私钥文件暴露到外网直接下载了哦
            FileInputStream in1 = new FileInputStream(privateKeyFile);
            in1.read(privateKey);
            in1.close();
            
            //读取公钥的内容
            FileInputStream in2 = new FileInputStream(publicKeyFile);
            in2.read(publicKey);
            in2.close();
            
        } catch (Exception e ) {
            e.printStackTrace();
        }
        
        WebRTCSigApi api = new WebRTCSigApi();
        api.setSdkAppid(sdkappid);
        api.setPrivateKey(new String(privateKey));
        api.setPublicKey(new String(publicKey));
        
        //生成userSig
        String userSig = api.genUserSig(userid, 300);
        
        //生成privateMapKey
        String privateMapKey = api.genPrivateMapKey(userid, roomid, 300);
        
        System.out.println("userSig:\n" + userSig);
        System.out.println("privateMapKey:\n" + privateMapKey);
    }*/
    
}

base64类

/*
 * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
 */
package com.tencent.liteav.demo.webrtc;

/**
 * Created by wangtianfei01 on 17/4/6.
 */

public class Base64 {

    private static char[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
            .toCharArray();
    private static byte[] codes = new byte[256];

    public static char[] encode(byte[] data) {
        char[] out = new char[((data.length + 2) / 3) * 4];
        for (int i = 0, index = 0; i < data.length; i += 3, index += 4) {
            boolean quad = false;
            boolean trip = false;
            int val = (0xFF & (int) data[i]);
            val <<= 8;
            if ((i + 1) < data.length) {
                val |= (0xFF & (int) data[i + 1]);
                trip = true;
            }
            val <<= 8;
            if ((i + 2) < data.length) {
                val |= (0xFF & (int) data[i + 2]);
                quad = true;
            }
            out[index + 3] = alphabet[(quad ? (val & 0x3F) : 64)];
            val >>= 6;
            out[index + 2] = alphabet[(trip ? (val & 0x3F) : 64)];
            val >>= 6;
            out[index + 1] = alphabet[val & 0x3F];
            val >>= 6;
            out[index + 0] = alphabet[val & 0x3F];
        }
        return out;
    }

    public static byte[] decode(char[] data) {
        int len = ((data.length + 3) / 4) * 3;
        if (data.length > 0 && data[data.length - 1] == '=') {
            --len;
        }
        if (data.length > 1 && data[data.length - 2] == '=') {
            --len;
        }
        byte[] out = new byte[len];
        int shift = 0;
        int accum = 0;
        int index = 0;
        for (int ix = 0; ix < data.length; ix++) {
            int value = codes[data[ix] & 0xFF];
            if (value >= 0) {
                accum <<= 6;
                shift += 6;
                accum |= value;
                if (shift >= 8) {
                    shift -= 8;
                    out[index++] = (byte) ((accum >> shift) & 0xff);
                }
            }
        }
        if (index != out.length) {
            throw new Error("miscalculated data length!");
        }
        return out;
    }

    static {
        for (int i = 0; i < 256; i++) {
            codes[i] = -1;
        }
        for (int i = 'A'; i <= 'Z'; i++) {
            codes[i] = (byte) (i - 'A');
        }
        for (int i = 'a'; i <= 'z'; i++) {
            codes[i] = (byte) (26 + i - 'a');
        }
        for (int i = '0'; i <= '9'; i++) {
            codes[i] = (byte) (52 + i - '0');
        }
        codes['+'] = 62;
        codes['/'] = 63;
    }
}

生成:


    private void ininsig() {
        int sdkappid = 1400162038;   //腾讯云云通信sdkappid 1400162038
        int roomid = 1234;           //音视频房间号roomid
        String userid = "webrtc98777";  //用户名userid

     /*   File privateKeyFile = new File("private_key");
        byte[] privateKey = new byte[(int)privateKeyFile.length()];

        File publicKeyFile = new File("public_key");
        byte[] publicKey = new byte[(int)publicKeyFile.length()];

        try {
            //读取私钥的内容
            //PS:不要把私钥文件暴露到外网直接下载了哦
            FileInputStream in1 = new FileInputStream(privateKeyFile);
            in1.read(privateKey);
            in1.close();

            //读取公钥的内容
            FileInputStream in2 = new FileInputStream(publicKeyFile);
            in2.read(publicKey);
            in2.close();

        } catch (Exception e ) {
            e.printStackTrace();
        }*/

        WebRTCSigApi api = new WebRTCSigApi();
        api.setSdkAppid(sdkappid);
//        api.setPrivateKey(new String(privateKey));
        api.setPrivateKey("-----BEGIN PRIVATE KEY-----\r\n"+"你的IM创建项目设置里下载获取的私钥MBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+eeR7R2n0cTcwmHj\r\n" + "你的IM创建项目设置里下载获取的私钥slopZXQycM/RznRQ/TjvaATNmbKcj\r\n" + "你的IM创建项目设置里下载获取的私钥mSNbq718jkTX\r\n"+"-----END PRIVATE KEY-----\r\n");
//        api.setPublicKey(new String(publicKey));

        //生成userSig
        String userSig = api.genUserSig(userid, 2000*50);
        SharedPreUtil.saveString(this,"userSig",userSig);
        System.out.println("privateMapKey:\n" + userSig);
     
        Toast.makeText(this, "sig:"+userSig, Toast.LENGTH_SHORT).show();
    }

建议你也可以自行阅读文档获取,因为这里面介绍了如何获取accountType 以及公钥私钥下载(后面用nodejs命令行注册腾讯云需要用到的,主要是在你下载的RoomTool 工具包里面的config.js文件修改):https://cloud.tencent.com/document/product/454/14548

image.png
image.png

点击 RoomTool.zip 下载腾讯云 RoomService 后台配置工具,这是一个基于 Node.js 的配置工具,需要您在使用前 安装 Node.js 。配置工具压缩包中包含的 pdf 和 PPT 有详细的配置说明,这里仅简要概括一下各个配置项的含义和作用。

修改config.js文件夹里面的请完全按照它的示例哦,不要以为那些提示是不用的了哈,如下图保留复制粘贴你的即可,


image.png

另外就是在生成userSig的时候,也是替换\t\r,文档里少了一个,坑死人了 会报一个错 ,注意哦。


image.png

config.js文件按照提示修改完毕后,按照nodejs环境后,注册腾讯云:打开命令行 cmd --,然后 运行:node setConfigInfo.js 1
根据提示运行后会显示请求成功的返回提示哦,如果你的私钥配置错误,会提示相应的错误,有时候提示不准确,还有就是你的播放域名要配置才可以哦。

到这里你如果还有什么问题你可以提交工单会有客服处理的或论坛搜索答案。

在调试的时候我们可以先写死:userId(注意如果用两个手机测试,这里需要改成不一样的否则IM或别踢出的报user not logged in),

image.png

直播SDK错误码对照表:https://cloud.tencent.com/document/product/454/8292
到这里就差不多了你可以用两个以上的手机(每个的userId都要唯一)同时在线调试直播并连麦了哈,希望这篇文章节省了你一些时间。谢谢。

image.png

头像是不是帅到你了哈哈笑死我了。。。


光写文章没有福利怎么可能,那是永远都不可能的啦。

1.公众号:推荐两个郭霖,鸿洋,微信搜索其名字即可。这两位都是知名博客大佬,但是关注公众号可以每天在打发早上坐地铁时间就吸收一波干货了,一举两得有木有。当然我知道在地铁里你只会关注美女的哈哈。。

2.Android 安卓UI 库集合推荐:当你的产品经理需要你自定义根据心情改变手机主题颜色的时候,你就应该想到到这里找找轮子了,收藏吧,骚年。

//www.greatytc.com/p/da1ca645b95c

3.工具类RxTools推荐:普通app几乎大部分工具类,有这个够用了。GitHub上7千+收藏量了。

https://github.com/vondear/RxTool

4.安卓博客门户:除了apkbus,掘金,简书,codekk也不错http://p.codekk.com/鸿洋的玩安卓http://www.wanandroid.com/

5.程序员的导航之家:极客导航,分类了你几乎想要了解的网址门户

http://www.jikedaohang.com/

5.阿里矢量icon图库:有了这个你是不是可以和ui妹子多交流交流呢哈哈

http://www.iconfont.cn/


得到全平台付费内容打包带走(共4T 含音频+图片及直播内容视频等)

你以为送完免费东东就完了,是不是还意犹未尽?没错,我必须给大伙点看家家当了,我本人收集了得到APP上面所有的付费内容,没错,不要问全不全,什么付费订阅课,大师课统统都有。我现在自己就每天都在听,很方便,路上在家有空的时候插上耳机收听就行了。感觉自己每天都有无形的收获!

做程序员这么多年,一辈子敲代码是不可能的,永远都不可能的。这会是程序员通往产品经理,程序员通往管理高层的成功必经之路!

image

比如说梁宁产品思维三十讲,宁向东的清华管理学等等,得到平台如果您不了解,你可以现在立刻马上电子市场下载,它上面的付费内容含金量个人觉得数一数二了!

上面的大师级别的课都上99,199起步的,现在我这里拿去,仅99元全部打包带走,加微信购买发百度云链接永久保存到您自己网盘!现在凭这篇文章截图可以送喜马拉雅全平台2017,2018付费音频!

image
image

一辈子敲代码是不可能的,我觉得我们程序员要获取的知识面应该更宽更广,得到平台上的内容很多就很符合,转型其他岗位,行业,但是机会总是留给做好准备的人!所以抓紧学习吧,趁还年轻!2018年定价99元,2019年错过这个村就就没这个价啦。

如果您担心音质问题,那是完全多余了,您可以5元视听一个栏目,不是百分百原音质退全额。

我的微信号(购买加微信必须注明简书得到):together13ly14

image

另外有腾讯课堂著名的动脑学院Android VIP全套课程视频源码(大概2T,高清原画质),价值6888元,不了解的可腾讯课堂搜Android课程排名第一的长沙--动脑学院,你官方报名同样很多时候是看视频的,讲师讲课很仔细,有源码demo,我这里白菜价拿走不谢,看完这套视频,工资上涨3-8k不在话下!高级安卓进阶必会技术需要的微信联系这里不私聊。

image.png

image.png

image.png

[我的小店进店链接]:dedaofree.taobao.com
淘口令:【混沌大大学堂王东岳创新院研习社商学院创业营创投营包更新】https://m.tb.cn/h.3NTTSIi 点击链接,再选择浏览器咑閞;或復·制这段描述¥Sgk7bPooUrp¥后到淘♂寳♀

欢迎收藏双击吐槽赞赏

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,451评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,691评论 2 59
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 八百天下阅读 318评论 0 0